https://github.com/python/cpython/commit/fd78b565d7c326f96ae903ab945b47f35d829cf4
commit: fd78b565d7c326f96ae903ab945b47f35d829cf4
branch: 3.10
author: Serhiy Storchaka <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-08-11T17:06:59+01:00
summary:
[3.10] gh-98820: Fix quadratic time in csv.Sniffer for quoted fields
(GH-154867) (#155539)
files:
A Misc/NEWS.d/next/Library/2026-07-29-11-20-00.gh-issue-98820.Qm7Hs4.rst
M Lib/csv.py
M Lib/test/test_csv.py
diff --git a/Lib/csv.py b/Lib/csv.py
index 5866f3796c6a9c..4e95bf9eb74326 100644
--- a/Lib/csv.py
+++ b/Lib/csv.py
@@ -215,12 +215,18 @@ def _guess_quote_and_delimiter(self, data, delimiters):
this way.
"""
+ # The body of a quoted field ends at the first quote which is
+ # not doubled, as it does for a reader. A lazy ".*?" scans to
+ # the end of the sample instead, from every start: quadratically.
+ # As an unrolled loop it is unambiguous, so it does not backtrack.
+ other = r'(?:(?!(?P=quote)).)*'
+ body = r'%s(?:(?P=quote){2}%s)*' % (other, other)
matches = []
- for restr in (r'(?P<delim>[^\w\n"\'])(?P<space>
?)(?P<quote>["\']).*?(?P=quote)(?P=delim)', # ,".*?",
-
r'(?:^|\n)(?P<quote>["\']).*?(?P=quote)(?P<delim>[^\w\n"\'])(?P<space> ?)', #
".*?",
- r'(?P<delim>[^\w\n"\'])(?P<space>
?)(?P<quote>["\']).*?(?P=quote)(?:$|\n)', # ,".*?"
- r'(?:^|\n)(?P<quote>["\']).*?(?P=quote)(?:$|\n)'):
# ".*?" (no delim, no space)
- regexp = re.compile(restr, re.DOTALL | re.MULTILINE)
+ for restr in (r'(?P<delim>[^\w\n"\'])(?P<space>
?)(?P<quote>["\'])%s(?P=quote)(?P=delim)', # ,"...",
+
r'(?:^|\n)(?P<quote>["\'])%s(?P=quote)(?P<delim>[^\w\n"\'])(?P<space> ?)',
# "...",
+ r'(?P<delim>[^\w\n"\'])(?P<space>
?)(?P<quote>["\'])%s(?P=quote)(?:$|\n)', # ,"..."
+ r'(?:^|\n)(?P<quote>["\'])%s(?P=quote)(?:$|\n)'):
# "..." (no delim, no space)
+ regexp = re.compile(restr % body, re.DOTALL | re.MULTILINE)
matches = regexp.findall(data)
if matches:
break
diff --git a/Lib/test/test_csv.py b/Lib/test/test_csv.py
index 6ff5bc3d644e69..f83929b5626939 100644
--- a/Lib/test/test_csv.py
+++ b/Lib/test/test_csv.py
@@ -1232,6 +1232,13 @@ def test_sniff_space_delimiter(self):
self.assertEqual(dialect.delimiter, ' ')
self.assertIs(dialect.doublequote, False)
+ def test_sniff_quoted_single_column(self):
+ # gh-98820: this sample used to take minutes.
+ sniffer = csv.Sniffer()
+ sample = '"abcdefghijklmnopqrstuvwxyz"\n' * 10000
+ with self.assertRaisesRegex(csv.Error, "Could not determine
delimiter"):
+ sniffer.sniff(sample, delimiters=',:|\t')
+
class NUL:
def write(s, *args):
diff --git
a/Misc/NEWS.d/next/Library/2026-07-29-11-20-00.gh-issue-98820.Qm7Hs4.rst
b/Misc/NEWS.d/next/Library/2026-07-29-11-20-00.gh-issue-98820.Qm7Hs4.rst
new file mode 100644
index 00000000000000..aa9ae8d937004f
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-29-11-20-00.gh-issue-98820.Qm7Hs4.rst
@@ -0,0 +1,2 @@
+Fix quadratic time in :meth:`csv.Sniffer.sniff` for a sample which contains
+quoted fields, in particular for a single column of quoted fields.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]