https://github.com/python/cpython/commit/b885859d097390d26764e956e4491539084d0d56
commit: b885859d097390d26764e956e4491539084d0d56
branch: 3.14
author: tonghuaroot (童话) <[email protected]>
committer: StanFromIreland <[email protected]>
date: 2026-08-25T19:53:39+01:00
summary:

[3.14] gh-152204: Validate date and time fields in 
`_pydatetime.date{time}.fromisoformat` (GH-152205) (#156367)

(cherry picked from commit e81960fb82977df7ace77f3c03a3b2d9a0559075)

files:
A Misc/NEWS.d/next/Library/2026-06-25-14-05-00.gh-issue-152204.k9Qm3v.rst
M Lib/_pydatetime.py
M Lib/test/datetimetester.py

diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py
index 0f95a5750848237..3a573d35bce2414 100644
--- a/Lib/_pydatetime.py
+++ b/Lib/_pydatetime.py
@@ -355,19 +355,30 @@ def _find_isoformat_datetime_separator(dtstr):
             return 8
 
 
+def _read_isoformat_component(s, n):
+    # The caller has verified the string is ASCII, so isdigit() matches only
+    # the ASCII digits accepted by the C parser.
+    if len(s) != n or not s.isdigit():
+        raise ValueError("Invalid isoformat string")
+    return int(s)
+
+
 def _parse_isoformat_date(dtstr):
     # It is assumed that this is an ASCII-only string of lengths 7, 8 or 10,
     # see the comment on 
Modules/_datetimemodule.c:_find_isoformat_datetime_separator
     if len(dtstr) not in (7, 8, 10):
         raise ValueError("Invalid isoformat string")
-    year = int(dtstr[0:4])
+    if not dtstr.isascii():
+        raise ValueError("Invalid isoformat string")
+
+    year = _read_isoformat_component(dtstr[0:4], 4)
     has_sep = dtstr[4] == '-'
 
     pos = 4 + has_sep
     if dtstr[pos:pos + 1] == "W":
         # YYYY-?Www-?D?
         pos += 1
-        weekno = int(dtstr[pos:pos + 2])
+        weekno = _read_isoformat_component(dtstr[pos:pos + 2], 2)
         pos += 2
 
         dayno = 1
@@ -377,17 +388,17 @@ def _parse_isoformat_date(dtstr):
 
             pos += has_sep
 
-            dayno = int(dtstr[pos:pos + 1])
+            dayno = _read_isoformat_component(dtstr[pos:pos + 1], 1)
 
         return list(_isoweek_to_gregorian(year, weekno, dayno))
     else:
-        month = int(dtstr[pos:pos + 2])
+        month = _read_isoformat_component(dtstr[pos:pos + 2], 2)
         pos += 2
         if (dtstr[pos:pos + 1] == "-") != has_sep:
             raise ValueError("Inconsistent use of dash separator")
 
         pos += has_sep
-        day = int(dtstr[pos:pos + 2])
+        day = _read_isoformat_component(dtstr[pos:pos + 2], 2)
 
         return [year, month, day]
 
@@ -402,10 +413,7 @@ def _parse_hh_mm_ss_ff(tstr):
     time_comps = [0, 0, 0, 0]
     pos = 0
     for comp in range(0, 3):
-        if (len_str - pos) < 2:
-            raise ValueError("Incomplete time component")
-
-        time_comps[comp] = int(tstr[pos:pos+2])
+        time_comps[comp] = _read_isoformat_component(tstr[pos:pos+2], 2)
 
         pos += 2
         next_char = tstr[pos:pos+1]
@@ -426,7 +434,7 @@ def _parse_hh_mm_ss_ff(tstr):
             raise ValueError("Invalid microsecond separator")
         else:
             pos += 1
-            if not all(map(_is_ascii_digit, tstr[pos:])):
+            if not tstr[pos:].isdigit():
                 raise ValueError("Non-digit values in fraction")
 
             len_remainder = len_str - pos
@@ -447,6 +455,8 @@ def _parse_isoformat_time(tstr):
     len_str = len(tstr)
     if len_str < 2:
         raise ValueError("Isoformat time too short")
+    if not tstr.isascii():
+        raise ValueError("Invalid isoformat string")
 
     # This is equivalent to re.search('[+-Z]', tstr), but faster
     tz_pos = (tstr.find('-') + 1 or tstr.find('+') + 1 or tstr.find('Z') + 1)
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index 1b71b71c544c0cb..ef7772e7e8a8c7b 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -2100,7 +2100,15 @@ def test_fromisoformat_fails(self):
             '10000-W25-1',      # Invalid year
             '2020-W25-0',       # Invalid day-of-week
             '2020-W25-8',       # Invalid day-of-week
-            '٢025-03-09'        # Unicode characters
+            # gh-152204: each fixed-width field must be exactly N ASCII digits
+            '2020+12',          # '+' in a basic-format field
+            '2020 12',          # space in a basic-format field
+            '+020-06-15',       # leading sign in the year
+            '202012+9',         # '+' in the day field
+            '2020-W 5',         # space in the week number
+            '2020061',          # 7 chars: day slice reads a 1-character tail
+            '2020-W2',          # 1-digit week number
+            '٢025-03-09',       # Unicode characters
             '2009\ud80002\ud80028',     # Separators are surrogate codepoints
         ]
 
@@ -3582,6 +3590,15 @@ def test_fromisoformat_fails_datetime(self):
             '2009-04-19T12:30:45.400 ',        # Trailing space (gh-130959)
             '2009-04-19T12:30:45. 400',        # Space before fraction 
(gh-130959)
             '2020-2020',                       # Ambiguous 9-char date portion
+            # gh-152204: each time field must be exactly N ASCII digits
+            '2020-12-12T0٥:02:03',          # Unicode digit in the hour
+            '2020-12-12T01:0٥:03',          # Unicode digit in the minute
+            '2020-12-12T01:02:0٥',          # Unicode digit in the second
+            '2020-12-12T01:02:03.٥',        # Unicode digit in the fraction
+            '2020-12-12T01:02:03.4_6',      # underscore in the fraction
+            '2020-12-12T01:02:03+0٥:00',    # Unicode digit in the tz hour
+            '2020-12-12T01:02:03+01:0٥',    # Unicode digit in the tz minute
+            '20201212T0102٣٤',              # Unicode digits in the 
basic-format time
             '2009-04-19T12:30:45.+05:00',      # Empty fraction before offset
             '2009-04-19T12:30:45.-05:00',      # Empty fraction before offset
             '2009-04-19T12:30:45.Z',           # Empty fraction before Z
diff --git 
a/Misc/NEWS.d/next/Library/2026-06-25-14-05-00.gh-issue-152204.k9Qm3v.rst 
b/Misc/NEWS.d/next/Library/2026-06-25-14-05-00.gh-issue-152204.k9Qm3v.rst
new file mode 100644
index 000000000000000..1ba1f872eb41a61
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-06-25-14-05-00.gh-issue-152204.k9Qm3v.rst
@@ -0,0 +1,6 @@
+Fix the pure-Python implementations of :meth:`datetime.date.fromisoformat`,
+:meth:`datetime.time.fromisoformat` and :meth:`datetime.datetime.fromisoformat`
+silently accepting some malformed ISO 8601 strings, such as non-ASCII digits or
+a sign in a fixed-width field (for example ``'2020+12'`` or 
``'20201212T0102٣٤'``).
+Each field is now required to be exactly *N* ASCII digits, matching the C
+implementation.

_______________________________________________
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]

Reply via email to