GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — gh-83273: Rewrite csv.Sniffer dialect detection using trial parsing by serhiy-storchaka · Pull Request #153694 · python/cpython

Conversation

@serhiy-storchaka

@serhiy-storchaka serhiy-storchaka commented Jul 14, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

Summary

csv.Sniffer.sniff() now guesses the dialect by parsing the sample with every plausible combination of delimiter, quotechar and escapechar, using the actual CSV parser in strict mode, and choosing the combination which splits the sample into rows with the most consistent number of fields. The result is consistent with how csv.reader() will parse the sample — by construction.

The old heuristics are removed. They guessed the delimiter from characters adjacent to quotes (_guess_quote_and_delimiter) and from character frequency statistics (_guess_delimiter), so a delimiter inside a quoted field could win over the actual one, sniffing took quadratic time on ordinary quoted data (20+ seconds on a ~1 MB sample, which looked like a hang), and an escape character could not be detected at all.

A large sample is parsed incrementally: first only its beginning, then, after eliminating the combinations which are clearly worse than the leader, a several times larger part, and so on. Combinations with a quotechar are not parsed until the quote character occurs at the start of a field. The verdict that a delimiter does not delimit anything is not trusted to the first part of the sample, which covers its least representative beginning (titles, comments, headers).

User-visible changes

  • csv.Sniffer can now detect escapechar='\\'.
  • Explicitly requested delimiters are no longer restricted to ASCII.
  • A delimiter inside a quoted field no longer wins over the actual delimiter, and sniffing no longer takes quadratic time on quoted samples.
  • The sample can be cut off at an arbitrary point: in the middle of a row, of a quoted field or of an escaped sequence.
  • Only '\r', '\n' and '\r\n' are treated as row separators, matching the reader; characters like '\x1c' can now be detected as a delimiter.
  • Letters and digits are no longer guessed as a delimiter.
  • A preamble (title or comment lines) before the data does not prevent detection if the data rows outnumber the preamble lines.
  • A sample consisting of a single column of quoted fields now raises csv.Error instead of guessing a delimiter from the content of the fields, and Sniffer.has_header() no longer raises ValueError for such samples.
  • doublequote is detected by comparing the two readings of the sample.

Results may differ from earlier Python versions on valid samples; this is documented with a versionchanged note and a What's New entry.

Performance

Release build, sniff() wall time:

sample old new
typical quoted CSV, 4 KB 0.1 ms 0.2 ms
quoted CSV, 64 KB 1.5 ms 2.3 ms
quoted CSV, 940 KB > 20 s 30 ms
single column of quoted fields, 1.1 MB minutes (gh-98820) 10 ms
random garbage, 1 MB 9 ms (meaningless answer) ~25 ms (Error)

The old regular expressions were quadratic on quoted data; the new code is linear in the sample size with a small factor.

Closed issues

  • Closes Csv sniffer doesn't attempt to determine and set escape character. #83273: escapechar could not be detected.
  • Closes csv.Sniffer().sniff() Hangs on Single-Column Data #98820: sniffing a single column of quoted fields took minutes.
  • Closes csv.Sniffer regular expression has significant backtracing #109638: catastrophic backtracking on an artificial sample.
  • Closes CSV sniffing detects wrong field delimiter #97611: a quoted tab won over the actual delimiter.
  • Closes The CSV file delimiter found with csv.Sniffer() is interpreted incorrectly #119123: commas inside bracketed lists won over the actual delimiter.
  • Closes CSV sniffing falsely detects space as a delimiter #88843: a space next to stray quotes was detected as the delimiter.
  • Closes csv.Sniffer.snif doesn't set up the dialect properly for a csv created with dialect=csv.excel_tab and containing quote (") char #62029: tab-delimited data with quotes was detected as space-delimited.
  • Closes In csv.py - guess_delimiter: add delimiters to list of eligible delimiters #111820: a non-ASCII delimiter in the delimiters argument was ignored.
  • Closes csv.py _guess_quote_and_delimiter should be able to handle windows \r #103925: a quote before '\r\n' broke detection (the delimiter was detected as a letter).
  • Closes Floating point rounding error in _guess_delimiter of csv module #111487: the floating-point threshold loop no longer exists.

Also addressed, at the triagers' discretion:

  • csv.Sniffer.has_header() leaks ValueError instead of csv.Error for an inconsistent guessed dialect #153634: fixed at the root — sniff("''") now raises csv.Error, so has_header() cannot leak ValueError.
  • csv.Sniffer uses a heuristic to determine doublequote that is often false-negative #109339: the doublequote false positive is fixed; the proposed default-to-True policy was deliberately not adopted — detection remains evidence-based.

Supersedes the stalled PRs gh-109639, gh-103926, gh-153635 and gh-27256, which patch the removed regular expressions. gh-109639 and gh-153635 remain valid as narrow backport candidates for the maintenance branches.

Notes for reviewers

  • No backports: results change on valid inputs, so this is for 3.16 only. The hang class can be fixed on maintenance branches by the narrow regex patches mentioned above.
  • escapechar detection is cleanly severable (one loop variant in candidate construction) if landing it separately is preferred; nine of the ten closed issues do not depend on it.
  • Detection of quoting (Feature request: sniff csv quoting and lineterminator #129374) and lineterminator (csv.Sniffer does not detect lineterminator #75008) is deliberately out of scope; the new structure has a natural place for both (_detect_* methods on the winning combination).
  • Issue: Csv sniffer doesn't attempt to determine and set escape character. #83273

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

dolfinus and BrenBarn reacted with thumbs up emoji
All reactions
  • 👍 2 reactions
@serhiy-storchaka @claude
pythongh-83273: Rewrite csv.Sniffer dialect detection using trial par…
7c9f890
…sing

Guess the dialect by parsing the sample with every plausible
combination of delimiter, quotechar and escapechar, using the actual
CSV parser in strict mode, and choosing the combination which splits
the sample into rows with the most consistent number of fields.  The
old heuristics, which guessed the delimiter from characters adjacent
to quotes and from character frequencies, are removed.

A large sample is parsed incrementally: first only its beginning,
then, after eliminating the combinations which are clearly worse than
the leader, a several times larger part, and so on.

* csv.Sniffer can now detect escapechar='\\'.
* Explicitly requested delimiters are no longer restricted to ASCII.
* A delimiter inside a quoted field no longer wins over the actual
  delimiter, and sniffing no longer takes quadratic time on quoted
  samples.
* The sample can be cut off at an arbitrary point: in the middle of a
  row, of a quoted field or of an escaped sequence.
* Only '\r', '\n' and '\r\n' are treated as row separators, so
  characters like '\x1c' can now be detected as a delimiter.
* A preamble (title or comment lines) before the data does not prevent
  detection if the data rows outnumber the preamble lines.
* A sample consisting of a single column of quoted fields now raises
  csv.Error instead of guessing a delimiter from the content of the
  fields, and Sniffer.has_header() no longer raises ValueError for
  such samples.
* doublequote is detected by comparing the two readings of the sample.

Co-Authored-By: Claude Fable 5 <[email protected]>
@serhiy-storchaka
serhiy-storchaka requested a review from AA-Turner as a code owner July 14, 2026 11:29
@bedevere-app bedevere-app Bot added the awaiting core review label Jul 14, 2026
@bedevere-app bedevere-app Bot mentioned this pull request Jul 14, 2026
Csv sniffer doesn't attempt to determine and set escape character. #83273
Closed
@read-the-docs-community

read-the-docs-community Bot commented Jul 14, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Documentation build overview

📚 cpython-previews | 🛠️ Build #33681272 | 📁 Comparing 9d465fd against main (357c650)

  🔍 Preview build  

50 files changed · ± 50 modified

± Modified

  • c-api/exceptions.html
  • c-api/frame.html
  • c-api/type.html
  • c-api/typeobj.html
  • c-api/veryhigh.html
  • faq/general.html
  • howto/abi3t-migration.html
  • howto/logging-cookbook.html
  • library/allos.html
  • library/archiving.html
  • and 40 more...
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@hugovk

hugovk commented Jul 19, 2026

Copy link
Copy Markdown
Member
  • No backports: results change on valid inputs, so this is for 3.16 only. The hang class can be fixed on maintenance branches by the narrow regex patches mentioned above.

I agree with this, let's just do 3.16. This behaviour change will likely benefit from the usual alpha testing phase.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@serhiy-storchaka
Merge branch 'main' into csv-sniffer-rework
9d465fd
# Conflicts:
#	Lib/csv.py
@serhiy-storchaka

serhiy-storchaka commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

This is based on my 2.5 years old patch. After re-basing it on main and cleaning up debug prints, it was able to fix 3 of existing issues when used as a fallback. When used as a primary method -- 6 more. After fixing few corner cases, the old engine was removed. Then I ordered Claude to implement other my old ideas -- using the real C parser for probing (my initial version was a parser implemented in Python via regexpes) and feeding data by parts to all candidates, with filtering out obvious loosers. This improved performance and also made detection more trustful.

hugovk reacted with hooray emoji
All reactions
  • 🎉 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@serhiy-storchaka
serhiy-storchaka merged commit 25fea6d into python:main Jul 21, 2026
53 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@bedevere-app bedevere-app Bot removed the awaiting core review label Jul 21, 2026
@serhiy-storchaka
serhiy-storchaka deleted the csv-sniffer-rework branch July 21, 2026 06:22
@Punisheroot Punisheroot mentioned this pull request Jul 29, 2026
[3.15] gh-109638: Avoid pathological backtracking in csv.Sniffer (GH-153694) #154865
Closed
@bedevere-app

bedevere-app Bot commented Jul 29, 2026

Copy link
Copy Markdown

GH-154865 is a backport of this pull request to the 3.15 branch.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@lpyu001 lpyu001 mentioned this pull request Sep 17, 2026
csv.Sniffer.sniff() raises Could not determine delimiter when the sample contains a field larger than field_size_limit() #157678
Open
@hugovk hugovk mentioned this pull request Oct 29, 2026
CSV dialect sniffer gets wrong dialect (excel) when tab delimited csv file has been truncated #110128
Closed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@AA-Turner AA-Turner Awaiting requested review from AA-Turner AA-Turner is a code owner

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

CSV sniffing detects wrong field delimiter CSV sniffing falsely detects space as a delimiter csv.Sniffer.snif doesn't set up the dialect properly for a csv created with dialect=csv.excel_tab and containing quote (") char csv.Sniffer().sniff() Hangs on Single-Column Data Csv sniffer doesn't attempt to determine and set escape character.
csv.py _guess_quote_and_delimiter should be able to handle windows \r csv.Sniffer regular expression has significant backtracing Floating point rounding error in _guess_delimiter of csv module In csv.py - guess_delimiter: add delimiters to list of eligible delimiters The CSV file delimiter found with csv.Sniffer() is interpreted incorrectly

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@serhiy-storchaka @hugovk
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.