From c0bd358bce3ff29c9d8a0676982976629e846263 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Thu, 9 Jul 2026 16:41:57 +0800 Subject: [PATCH 1/3] gh-153634: Raise csv.Error, not ValueError, from csv.Sniffer for an inconsistent guessed dialect csv.Sniffer.sniff could return a dialect whose delimiter equaled its quote character. Using that dialect (directly, or via csv.Sniffer.has_header, which builds a reader from the sniffed dialect) leaked a raw ValueError from the _csv extension instead of the module's own csv.Error. sniff now validates its guessed dialect before returning it and re-raises the _csv ValueError as csv.Error, so both sniff and has_header report the module exception type. The direct csv.reader and csv.writer paths and the Dialect._validate TypeError/ValueError behavior are unchanged. --- Lib/csv.py | 8 ++++++++ Lib/test/test_csv.py | 10 ++++++++++ .../2026-07-09-16-41-20.gh-issue-153634.27Fs4T.rst | 4 ++++ 3 files changed, 22 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-07-09-16-41-20.gh-issue-153634.27Fs4T.rst diff --git a/Lib/csv.py b/Lib/csv.py index b2aaf5fd9fa91e6..781cec29cc4d3f4 100644 --- a/Lib/csv.py +++ b/Lib/csv.py @@ -264,6 +264,14 @@ class dialect(Dialect): dialect.quotechar = quotechar or '"' dialect.skipinitialspace = skipinitialspace + # A guess can be inconsistent (for example delimiter == quotechar). + # Building a reader validates it; re-raise the _csv ValueError as a + # csv.Error so callers see the module's own exception type. + try: + reader(StringIO(), dialect) + except ValueError as e: + raise Error(str(e)) from None + return dialect diff --git a/Lib/test/test_csv.py b/Lib/test/test_csv.py index 7327c1bd5f50530..0e7bbe8a4a2f2a7 100644 --- a/Lib/test/test_csv.py +++ b/Lib/test/test_csv.py @@ -1463,6 +1463,16 @@ def test_sniff(self): self.assertEqual(dialect.quotechar, "'") self.assertIs(dialect.skipinitialspace, False) + def test_sniff_delimiter_equals_quotechar(self): + # A sample that makes sniff() guess a dialect whose delimiter equals + # its quotechar is invalid; that must surface as csv.Error, not as a + # raw ValueError leaking from the _csv extension. + sniffer = csv.Sniffer() + for sample in ('"', '""', "''"): + with self.subTest(sample=sample): + self.assertRaises(csv.Error, sniffer.sniff, sample) + self.assertRaises(csv.Error, sniffer.has_header, sample) + def test_delimiters(self): sniffer = csv.Sniffer() dialect = sniffer.sniff(self.sample3) diff --git a/Misc/NEWS.d/next/Library/2026-07-09-16-41-20.gh-issue-153634.27Fs4T.rst b/Misc/NEWS.d/next/Library/2026-07-09-16-41-20.gh-issue-153634.27Fs4T.rst new file mode 100644 index 000000000000000..80ead93b964ef2f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-09-16-41-20.gh-issue-153634.27Fs4T.rst @@ -0,0 +1,4 @@ +:meth:`csv.Sniffer.sniff` now raises :exc:`csv.Error` for an inconsistent +guessed dialect, for example one whose delimiter equals its quote character, +instead of returning it. :meth:`csv.Sniffer.has_header` now raises +:exc:`csv.Error` instead of leaking a :exc:`ValueError`. From ebb5be4ac853b674d530dec316155b067e8b7618 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Mon, 13 Jul 2026 09:45:40 +0800 Subject: [PATCH 2/3] Simplify the guessed-dialect validation comment --- Lib/csv.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/csv.py b/Lib/csv.py index 781cec29cc4d3f4..3caeaed33191962 100644 --- a/Lib/csv.py +++ b/Lib/csv.py @@ -264,9 +264,8 @@ class dialect(Dialect): dialect.quotechar = quotechar or '"' dialect.skipinitialspace = skipinitialspace - # A guess can be inconsistent (for example delimiter == quotechar). - # Building a reader validates it; re-raise the _csv ValueError as a - # csv.Error so callers see the module's own exception type. + # A guessed dialect can be inconsistent (delimiter == quotechar); + # surface it as csv.Error, not a raw _csv ValueError. try: reader(StringIO(), dialect) except ValueError as e: From 07d781f885af90ed894d45cecc6f27dd1cd013e5 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Mon, 13 Jul 2026 09:49:23 +0800 Subject: [PATCH 3/3] Trim the test comment and tighten the NEWS wording --- Lib/test/test_csv.py | 4 +--- .../Library/2026-07-09-16-41-20.gh-issue-153634.27Fs4T.rst | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_csv.py b/Lib/test/test_csv.py index 0e7bbe8a4a2f2a7..84ba569e870e736 100644 --- a/Lib/test/test_csv.py +++ b/Lib/test/test_csv.py @@ -1464,9 +1464,7 @@ def test_sniff(self): self.assertIs(dialect.skipinitialspace, False) def test_sniff_delimiter_equals_quotechar(self): - # A sample that makes sniff() guess a dialect whose delimiter equals - # its quotechar is invalid; that must surface as csv.Error, not as a - # raw ValueError leaking from the _csv extension. + # An inconsistent guessed dialect must surface as csv.Error. sniffer = csv.Sniffer() for sample in ('"', '""', "''"): with self.subTest(sample=sample): diff --git a/Misc/NEWS.d/next/Library/2026-07-09-16-41-20.gh-issue-153634.27Fs4T.rst b/Misc/NEWS.d/next/Library/2026-07-09-16-41-20.gh-issue-153634.27Fs4T.rst index 80ead93b964ef2f..f70943b18dd73ad 100644 --- a/Misc/NEWS.d/next/Library/2026-07-09-16-41-20.gh-issue-153634.27Fs4T.rst +++ b/Misc/NEWS.d/next/Library/2026-07-09-16-41-20.gh-issue-153634.27Fs4T.rst @@ -1,4 +1,4 @@ :meth:`csv.Sniffer.sniff` now raises :exc:`csv.Error` for an inconsistent -guessed dialect, for example one whose delimiter equals its quote character, -instead of returning it. :meth:`csv.Sniffer.has_header` now raises -:exc:`csv.Error` instead of leaking a :exc:`ValueError`. +guessed dialect (one whose delimiter equals its quote character) instead of +returning it. :meth:`csv.Sniffer.has_header` now raises :exc:`csv.Error` +instead of leaking a :exc:`ValueError`.