[3.15] gh-98820: Fix quadratic time in csv.Sniffer for quoted fields (GH-154867)

serhiy-storchaka <[email protected]> Mon, 03 Aug 2026 08:26:33 -0400 (EDT)
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/476fb09cdb0d73e645849d98c610e7e5697ce7c9
commit: 476fb09cdb0d73e645849d98c610e7e5697ce7c9
branch: 3.15
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-03T12:26:19Z
summary:

[3.15] gh-98820: Fix quadratic time in csv.Sniffer for quoted fields (GH-154867)

The regular expressions which look for a quoted field matched its body
lazily, so a closing quote which was not followed by a delimiter was
retried with every following quote, to the end of the sample.  Match
the body possessively instead: it ends at the first quote which is not
doubled, as it does for a reader.

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>

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 8dbe0bdd598f0c..4940aa28dcd42d 100644
--- a/Lib/csv.py
+++ b/Lib/csv.py
@@ -286,12 +286,16 @@ def _guess_quote_and_delimiter(self, data, delimiters):
         """
         import re
 
+        # 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.
+        body = r'(?:(?P=quote){2}|(?!(?P=quote)).)*+'
         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)(?:$|\r|\n)',  # ,".*?"
-                      r'(?:^|\n)(?P<quote>["\']).*?(?P=quote)(?:$|\r|\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 30409450eb6372..5c2789632291bb 100644
--- a/Lib/test/test_csv.py
+++ b/Lib/test/test_csv.py
@@ -1607,6 +1607,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]