csv.Sniffer().has_header() False Negative (tested)
csv.Sniffer().has_header(sample) is meant to tell you whether a CSV's first row is a header. We fed it "fruit,color\napple,red\nbanana,yellow\n" — an unambiguous header over text data — and it returned False. We measured exactly why, and it comes down to a heuristic that most ordinary CSVs don't satisfy.
1. A real header, reported as no header
import csv
sample = "fruit,color\napple,red\nbanana,yellow\n"
csv.Sniffer().has_header(sample)
False
Measured: this is as clean a header row as CSV data gets — column names, then rows of matching text data — and has_header() says False.
2. A case it gets right: header types differ from the body
sample2 = "name,age,city\nAlice,30,NYC\nBob,25,LA\n"
csv.Sniffer().has_header(sample2)
True
Measured: this one correctly returns True. The difference from example 1 is that column 2 (age) is text in the header row but numeric in every data row — has_header()'s actual algorithm checks whether each column's apparent type or length in row 1 differs from the following rows, and flags a header only when it finds that contrast. fruit/color over apple/red has no such contrast: every value in every row is plain text, so there's nothing for the heuristic to detect.
3. A numeric-looking header fails the same way
sample3 = "2020,2021,2022\n100,200,150\n300,100,250\n"
csv.Sniffer().has_header(sample3)
False
Measured: year columns as headers, followed by numeric data — a real, common header row — also comes back False, for the same root reason: the header row and the data rows are both all-numeric, so no type contrast exists between row 1 and the rest.
4. Both are false negatives, not a quirk of one sample
Across these tests, has_header() only answered True for the one sample where the header's column types genuinely differed from the data's. That is a narrower condition than "the first row is a header" — most real CSVs have simple word headers (fruit, color, name when every column happens to be text, or numeric headers like years, IDs, or codes) that will not trigger it. A caller treating a False result as "there is no header row, start parsing from row 0" will silently include the header text as a data row.
5. What to actually do
Don't rely on has_header() alone to decide whether to skip the first row — it only reliably detects a header when that header's column types visibly differ from the body, which is the exception rather than the rule for hand-labeled CSVs. If you control the source, make the header-or-not decision explicit (a flag, a filename convention, or your own check for whether row 1 contains any value matching a numeric/date pattern that also appears identically in column position later). If you don't control the source, a cheap practical check is to see whether row 1 is unique among the first several rows (headers rarely repeat as data), rather than trusting this one heuristic.
Frequently asked questions
Why does csv.Sniffer().has_header() return False for an obvious header row?
We measured that has_header() specifically checks whether each column's type or length in row 1 differs from the following rows — it does not check anything else about whether row 1 'looks like' a header. A header row like 'fruit,color' over text data like 'apple,red' has no type contrast at all, so it returns False even though row 1 clearly is a header.
When does csv.Sniffer().has_header() correctly return True?
We confirmed it returns True when at least one column's apparent type differs between the header row and the data rows — for example a text header name like 'age' sitting above numeric values like 30, 25 in every data row.
Does a numeric-looking header (like year columns) get detected correctly?
No — we measured that a header row of '2020,2021,2022' over purely numeric data also returns False, for the same reason: the header row and every data row are all-numeric, so there's no type contrast for the heuristic to find.
Is has_header() reliable for deciding whether to skip the first CSV row?
Based on what we measured, no — it only reliably flags a header when the header's column types visibly differ from the data's, which is narrower than 'row 1 is a header.' Most real-world CSVs with simple same-type headers will get a False result even though a header genuinely exists.
What's a better way to detect a CSV header than csv.Sniffer().has_header()?
We'd make the decision explicit where possible (a known format, a filename convention, or an app-level flag) rather than relying on this one heuristic, since it's only accurate for the specific case where header and data column types differ.
Written with AI assistance. Measured on 2026-10-03 with Python 3.13.5's built-in csv module, calling csv.Sniffer().has_header() on CSV samples with a genuine text header row over text data, and a genuine numeric-looking header row (year columns) over numeric data, recording the exact True/False result for each. PlainHeaders is our extension; Python's csv module is built in.