GeezerVison is showing github.com — gh-137627: Make `csv.Sniffer.sniff()` delimiter detection 1.6x faster by maurycy · Pull Request #137628 · python/cpython
Conversation
maurycy
commented
Aug 11, 2025
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
The basic idea is not to iterate over all ASCII characters and count their frequency on each line in _guess_delimiter but only over present characters, and just backfill zeros.
Benchmark
There is no csv.Sniffer benchmark in pyperformance, so I created a simple benchmark instead:
import csv, pathlib, pyperf
def sniff(s): csv.Sniffer()._guess_delimiter(s, None)
r = pyperf.Runner()
sizes = [1024, 2048, 4096]
for file in pathlib.Path("/home/maurycy/CSVsniffer/CSV/").glob("*.csv"):
for s in sizes:
with file.open() as f:
try:
r.bench_func(f"csv_sniff({file.name}, {s})", sniff, f.read(s))
except UnicodeDecodeError:
pass
using all 149 files from CSVSniffer (MIT License), reading only the sample, as recommended in docs.python.org example. That's what real users do, too. Unfortunately, it takes a few hours to run.
The result:
Geometric mean: 1.60x faster
The full results (compare_to --table --table-format=md, and JSON files):
Correctness
I created a simple script to confirm that the output of Sniffer()._guess_delimiter() did not change. It uses ~1100 CSV files from the CSVSniffer project (MIT License).
The script:
from pathlib import Path
import csv_96b7a2eba423b42320f15fd4974740e3e930bb8b as csv_original
import csv as csv_modified
# https://github.com/ws-garcia/CSVsniffer
CSV_DIR_PATH = Path("/home/maurycy/CSVsniffer/")
SAMPLE = 1024
csv_files = list(CSV_DIR_PATH.rglob("*.[cC][sS][vV]"))
def differ(path, original, modified):
with open(path, "r", encoding="utf-8", errors="ignore") as f:
data = f.read(SAMPLE)
return original(data) != modified(data)
original_guess_delimiter = (
lambda data: csv_original.Sniffer()._guess_delimiter(data, delimiters=None)
)
modified_guess_delimiter = (
lambda data: csv_modified.Sniffer()._guess_delimiter(data, delimiters=None)
)
differences, total = (
sum(
differ(path, original_guess_delimiter, modified_guess_delimiter)
for path in csv_files
),
len(csv_files),
)
print(f"{total} CSV files in '{CSV_DIR_PATH}', {differences} differences.")
where Lib/csv_96b7a2eba423b42320f15fd4974740e3e930bb8b.py is https://github.com/python/cpython/blob/96b7a2eba423b42320f15fd4974740e3e930bb8b/Lib/csv.py from the main branch, and:
% git rev-parse HEAD
8614756ea48060aa8c4d4c1508b0221e1b50d263
% git status
## csv-sniffer-counter-set...origin/csv-sniffer-counter-set
?? Lib/csv_96b7a2eba423b42320f15fd4974740e3e930bb8b.py
?? csv-sniffer-diff.py
The result:
% ./python csv-sniffer-diff.py
1118 CSV files in '/home/maurycy/CSVsniffer/', 0 differences.
Environment
% ./python -c "import sysconfig; print(sysconfig.get_config_var('CONFIG_ARGS'))"
'--with-lto' '--enable-optimizations'
sudo ./python -m pyperf system tune ensured.
Notes
- The optimization is in
csv.Sniffer()._read_delimiter() which runs only if regular expressions in csv.Sniffer()._guess_quote_and_delimiter() failed, so there's no guarantee that csv.Sniffer().sniff() will always be faster
- My original patch relied on confusing set operations.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
reneleonhardt reacted with heart emoji
maurycy
changed the title
gh-137627: Make csv.Sniffer._guess_delimiter() 2x faster
gh-137627: Make csv.Sniffer.sniff() 2x faster
Aug 11, 2025
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are the benchmarks done with a POG+LTO build?
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
AA-Turner
left a comment
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a CSV expert, but here is a cursory review of the set logic. You should provide a (range of) benchmarks to back up the claim that it is twice as fast, though, ideally using pyperformance.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
maurycy
commented
Aug 11, 2025
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
@picnixz @AA-Turner I really appreciate your feedback! It's great. I will provide more benchmarks, including with enabled optimizations and ideally with pyperformance, rephrase NEWS, and add in a whatsnew.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Benchmarks without optimizations are not relevant so just run those with.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
maurycy
changed the title
gh-137627: Make csv.Sniffer.sniff() 2x faster
gh-137627: Make csv.Sniffer.sniff() delimiter detection 1.5x faster
Aug 13, 2025
maurycy
commented
Aug 13, 2025
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
@picnixz @AA-Turner @ZeroIntensity
Thank you for all the comments:
- I created a much more rigorous benchmark than before, see the results: https://github.com/maurycy/cpython-test/tree/main/csv-sniffer-counter-set The benchmark is now on par with how people use the class.
- I've changed the approach, ditching confusing set operations. The speed up now comes mostly from not iterating over
ascii over and over, and more efficient zero backfilling.
- There is nothing about
csv.Sniffer() in pyperformance, so I was a bit stuck here.
- I updated the docs.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
maurycy
commented
Aug 19, 2025
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
@picnixz
Thank you for your yesterday's review:
- I renamed the variables and simplified the zero-bucket, as per your suggestion,
- I added the tests for the tie break (
insert v. append of the zero bucket) to confirm that the behavior has not changed,
- I updated the docs and the benchmark (it's now 1.6x faster!)
Thank you!
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Love the enthusiasm, but please try to avoid continuously rebasing (pressing "update branch"). It wastes CI time and also puts a ding in all of our inboxes. Updating the branch should generally only be done to resolve merge conflicts.
|
maurycy reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
maurycy
commented
Aug 24, 2025
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
FYI: I created a simple check ensuring that the output of csv.Sniffer()._guess_delimiter() did not change:
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, looks good but please add a test with corner cases when the guessing algorithm makes multiple rounds.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks fine to me but I want another core dev to have a look just in case I missed some obvious things (maybe @serhiy-storchaka?)
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
maurycy reacted with heart emoji
maurycy
commented
Sep 8, 2025
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
maurycy reacted with heart emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
⚠️⚠️⚠️ Buildbot failure ⚠️⚠️⚠️
Hi! The buildbot AMD64 Windows11 Bigmem 3.x (tier-1) has failed when building commit 6be6f8f.
What do you need to do:
- Don't panic.
- Check the buildbot page in the devguide if you don't know what the buildbots are or how they work.
- Go to the page of the buildbot that failed (https://buildbot.python.org/#/builders/1079/builds/7354) and take a look at the build logs.
- Check if the failure is related to this commit (6be6f8f) or if it is a false positive.
- If the failure is related to this commit, please, reflect that on the issue and make a new Pull Request with a fix.
You can take a look at the buildbot page here:
https://buildbot.python.org/#/builders/1079/builds/7354
Failed tests:
Failed subtests:
- testZip64LargeFile - test.test_zipimport.UncompressedZipImportTestCase.testZip64LargeFile
- testZip64LargeFile - test.test_zipimport.ZStdCompressedZipImportTestCase.testZip64LargeFile
- testZip64LargeFile - test.test_zipimport.DeflateCompressedZipImportTestCase.testZip64LargeFile
Summary of the results of the build (if available):
==
Click to see traceback logs
Traceback (most recent call last):
File "R:\buildarea\3.x.ambv-bb-win11.bigmem\build\Lib\test\test_zipimport.py", line 934, in testZip64LargeFile
with open(os_helper.TESTFN, "wb") as f:
~~~~^^^^^^^^^^^^^^^^^^^^^^^^
OSError: [Errno 28] No space left on device
Traceback (test.test_zipimport.DeflateCompressedZipImportTestCase.testTraceback) ... ok
Traceback (test.test_zipimport.UncompressedZipImportTestCase.testTraceback) ... ok
Traceback (most recent call last):
File "R:\buildarea\3.x.ambv-bb-win11.bigmem\build\Lib\test\test_zipimport.py", line 997, in testZip64LargeFile
with open(TEMP_ZIP, "wb") as f:
~~~~^^^^^^^^^^^^^^^^
OSError: [Errno 28] No space left on device
Traceback (most recent call last):
File "R:\buildarea\3.x.ambv-bb-win11.bigmem\build\Lib\test\test_zipimport.py", line 1000, in testZip64LargeFile
f.seek(offset, os.SEEK_SET)
~~~~~~^^^^^^^^^^^^^^^^^^^^^
OSError: [Errno 28] No space left on device
Traceback (test.test_zipimport.ZStdCompressedZipImportTestCase.testTraceback) ... ok
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Buildbot failure unrelated: "OSError: [Errno 28] No space left on device".
I'll inform the owner.
|
maurycy reacted with heart emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Milestone
No milestone
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
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.
|