diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 076183139b28ae..941bb54f04a928 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -396,6 +396,10 @@ def _parse_isoformat_date(dtstr): pos += has_sep dayno = _read_isoformat_component(dtstr[pos:pos + 1], 1) + pos += 1 + + if pos != len(dtstr): + raise ValueError("Invalid isoformat string") return list(_isoweek_to_gregorian(year, weekno, dayno)) else: @@ -406,6 +410,10 @@ def _parse_isoformat_date(dtstr): pos += has_sep day = _read_isoformat_component(dtstr[pos:pos + 2], 2) + pos += 2 + + if pos != len(dtstr): + raise ValueError("Invalid isoformat string") return [year, month, day] @@ -1984,7 +1992,7 @@ def fromisoformat(cls, date_string): raise ValueError( f'Invalid isoformat string: {date_string!r}') from None - if tstr: + if separator_location < len(date_string): try: (time_components, became_next_day, diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 53285d918c17ca..a9970031cd374c 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2236,6 +2236,10 @@ def test_fromisoformat_fails(self): '2020-W 5', # space in the week number '2020061', # 7 chars: day slice reads a 1-character tail '2020-W2', # 1-digit week number + # gh-158500: trailing characters must not be ignored + '2020010112', # YYYYMMDD followed by two characters + '2020W011xx', # YYYYWwwD followed by two characters + '20200101\u00e9', # 9 characters, but 10 bytes in UTF-8 '٢025-03-09', # Unicode characters '2009\ud80002\ud80028', # Separators are surrogate codepoints ] @@ -3977,6 +3981,11 @@ def test_fromisoformat_fails_datetime(self): '2009-04-19T12:30:45.-05:00', # Empty fraction before offset '2009-04-19T12:30:45.Z', # Empty fraction before Z '2009-04-19T12:30:45,+05:00', # Empty fraction (comma) before offset + # gh-158500: a separator must be followed by a time + '2009-04-19T', # Separator without a time + '20090419T', # Separator without a time + '2009-W16-7T', # Separator without a time + '2009W167T', # Separator without a time ] for bad_str in bad_strs: diff --git a/Misc/NEWS.d/next/Library/2026-10-26-21-37-42.gh-issue-158500.zxyuq5.rst b/Misc/NEWS.d/next/Library/2026-10-26-21-37-42.gh-issue-158500.zxyuq5.rst new file mode 100644 index 00000000000000..52fb55d859fda7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-26-21-37-42.gh-issue-158500.zxyuq5.rst @@ -0,0 +1,5 @@ +:meth:`datetime.date.fromisoformat` now raises :exc:`ValueError` for a +basic-format date followed by extra characters, such as ``'2020010112'``, +instead of silently ignoring them. The pure-Python implementation of +:meth:`datetime.datetime.fromisoformat` now also rejects a date followed by a +single character, such as ``'2020-01-01T'``, matching the C implementation. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 959abcf5e421cf..380d8b435e73d6 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -977,6 +977,10 @@ parse_isoformat_date(const char *dtstr, const size_t len, int *year, int *month, iso_day = 1; } + if ((size_t)(p - dtstr) != len) { + return -1; + } + int rv = iso_to_ymd(*year, iso_week, iso_day, year, month, day); if (rv) { return -3 + rv; @@ -997,6 +1001,9 @@ parse_isoformat_date(const char *dtstr, const size_t len, int *year, int *month, if (p == NULL) { return -1; } + if ((size_t)(p - dtstr) != len) { + return -1; + } return 0; }