Skip to content

gh-158500: Reject trailing characters in date.fromisoformat() - #160737

Open
iamsaugatpandey wants to merge 1 commit into
python:mainfrom
iamsaugatpandey:gh-158500-date-fromisoformat-trailing
Open

iamsaugatpandey wants to merge 1 commit into
python:mainfrom
iamsaugatpandey:gh-158500-date-fromisoformat-trailing

Conversation

@iamsaugatpandey

Copy link
Copy Markdown

date.fromisoformat() accepts strings of length 7, 8 or 10, but neither parser checks that it read the whole string. A 10-character string without a - after the year goes down the basic-format path, which reads YYYYMMDD (or YYYYWwwD) and returns, ignoring the last two characters. So a truncated or mistyped timestamp turns into a plausible but wrong date:

>>> from datetime import date
>>> date.fromisoformat('2020010112')    # e.g. a YYYYMMDDHH stamp
datetime.date(2020, 1, 1)
>>> date.fromisoformat('2020W011xx')
datetime.date(2019, 12, 30)
>>> date.fromisoformat('20200101é')     # C only: 9 characters, but 10 bytes in UTF-8
datetime.date(2020, 1, 1)

With this change, all three raise ValueError: Invalid isoformat string, as datetime.fromisoformat() already does for the same input.

The fix checks that the parser stopped exactly at the end of the date, in both parse_isoformat_date() in C and _parse_isoformat_date() in _pydatetime. datetime.fromisoformat() uses the same function for its date part, but there the date always ends at the separator for valid strings. I compared the C module before and after on ~300k fuzzed strings: only date.fromisoformat() results changed (all from accepted to rejected), and datetime.fromisoformat() results did not change at all.

While adding the tests I found that the pure-Python datetime.fromisoformat() has a similar problem. It skipped time parsing whenever the time part was empty, so a date followed by any single character was accepted:

>>> import _pydatetime
>>> _pydatetime.datetime.fromisoformat('2020-01-01T')
datetime.datetime(2020, 1, 1, 0, 0)
>>> _pydatetime.datetime.fromisoformat('2021-01-071')
datetime.datetime(2021, 1, 7, 0, 0)

The C implementation already rejects these. It now parses the time whenever something follows the date, the same as C. It's the same "didn't consume the whole input" bug, so I kept it here, but I'm happy to split it out if you'd prefer.

Tests are added to test_fromisoformat_fails and test_fromisoformat_fails_datetime, so they run against both implementations.

date.fromisoformat() accepts strings of length 7, 8 or 10, but the date
parsers never checked that the whole string was consumed. A 10-character
string without a "-" after the year was parsed as an 8-character basic
format date and its last two characters were dropped, so
date.fromisoformat('2020010112') returned date(2020, 1, 1).

Check that the parser stopped exactly at the end of the date in both the
C and pure-Python implementations.

The pure-Python datetime.fromisoformat() had a similar problem: a date
followed by a single character (such as '2020-01-01T') was accepted
because the empty time part was skipped. Parse the time whenever
something follows the date, as the C implementation does.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@iamsaugatpandey
iamsaugatpandey requested a balanced review from Copilot October 26, 2026 21:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants