Skip to content

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

Merged
serhiy-storchaka merged 3 commits into
python:3.15from
serhiy-storchaka:sniff-315-gh98820
Aug 3, 2026
Merged

[3.15] gh-98820: Fix quadratic time in csv.Sniffer for quoted fields#154867
serhiy-storchaka merged 3 commits into
python:3.15from
serhiy-storchaka:sniff-315-gh98820

Conversation

@serhiy-storchaka

@serhiy-storchaka serhiy-storchaka commented Jul 29, 2026

Copy link
Copy Markdown
Member

The four regular expressions which look for a quoted field match its body lazily. A closing quote which is not followed by a delimiter is therefore retried with every following quote, to the very end of the sample, and findall() repeats that from every start position -- so the search is quadratic. On a single column of quoted fields none of them ever matches, so the whole cost is a failing scan.

>>> sample = '"abcdefghijklmnopqrstuvwxyz"\n' * 30000
>>> csv.Sniffer().sniff(sample, delimiters=',:|\t')   # minutes

The body now ends at the first quote which is not doubled, which is the only closing quote a reader would accept, and is matched possessively.

rows      before      after
 4000    3.1264 s    0.0427 s
30000        --      0.3190 s

A minimal reproducer needs no quoted fields at all -- ',"x' * n is quadratic too, because two of the four patterns are anchored on a delimiter rather than a line.

Being unambiguous is what makes it linear, so a few matches necessarily change: 16 of 560 files in the CSVsniffer corpora. Against their ground truth the net is positive -- delimiter 73.3% -> 74.5%, quotechar 81.3% -> 81.1%, doublequote unchanged.

main is not affected: the sniffer was rewritten there in gh-83273.

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.
serhiy-storchaka and others added 2 commits August 3, 2026 13:33
sniff() now replaces \r\n and \r with \n before these regular
expressions are used.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@serhiy-storchaka
serhiy-storchaka enabled auto-merge (squash) August 3, 2026 11:49
@serhiy-storchaka serhiy-storchaka added the type-security A security issue label Aug 3, 2026
@serhiy-storchaka
serhiy-storchaka merged commit 476fb09 into python:3.15 Aug 3, 2026
147 of 152 checks passed
@miss-islington-app

Copy link
Copy Markdown

Thanks @serhiy-storchaka for the PR 🌮🎉.. I'm working now to backport this PR to: 3.13, 3.14.
🐍🍒⛏🤖 I'm not a witch! I'm not a witch!

@bedevere-app

bedevere-app Bot commented Aug 3, 2026

Copy link
Copy Markdown

GH-155117 is a backport of this pull request to the 3.14 branch.

@bedevere-app bedevere-app Bot removed the needs backport to 3.14 bugs and security fixes label Aug 3, 2026
@bedevere-app

bedevere-app Bot commented Aug 3, 2026

Copy link
Copy Markdown

GH-155118 is a backport of this pull request to the 3.13 branch.

@bedevere-app bedevere-app Bot removed the needs backport to 3.13 bugs and security fixes label Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type-security A security issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant