Bug report
datetime.fromisoformat() reaches days_in_month() with month == 0 when the string combines an out-of-range (below 1) month with a 24:00 time, tripping a debug-build assertion. On a release build the same input raises ValueError, so the behaviour is inconsistent between build configurations.
Reproduction
>>> from datetime import datetime
>>> datetime.fromisoformat("2009-00-01T24:00:00")
On a --with-pydebug build (C implementation):
Assertion failed: (month >= 1), function days_in_month, file Modules/_datetimemodule.c, line 441.
(SIGABRT). The pure-Python implementation in Lib/_pydatetime.py raises AssertionError on the same input. On a release / -DNDEBUG build, both implementations correctly raise ValueError: month must be in 1..12, not 0.
Root cause
The 24:00 midnight-rollover guard validates the upper month bound but not the lower one before calling days_in_month():
Modules/_datetimemodule.c, datetime_datetime_fromisoformat_impl: if ((hour == 24) && (month <= 12))
Lib/_pydatetime.py, datetime.fromisoformat: if month <= 12 and day <= (days_in_month := _days_in_month(year, month))
The parser does not range-check the month before this point, so month == 0 flows into days_in_month(year, 0) and hits assert(month >= 1). The matching upper-bound input "2009-13-01T24:00:00" is already rejected by the month <= 12 check; only the lower bound is missing.
Suggested fix
Add the month >= 1 lower bound to the guard in both implementations (mirroring check_date_args's existing month < 1 || month > 12 validation), so the 24:00 path is only taken for an in-range month and the normal ValueError is raised otherwise. I'll open a PR.
This was found while adding a fuzz harness for fromisoformat.
Linked PRs
Bug report
datetime.fromisoformat()reachesdays_in_month()withmonth == 0when the string combines an out-of-range (below 1) month with a24:00time, tripping a debug-build assertion. On a release build the same input raisesValueError, so the behaviour is inconsistent between build configurations.Reproduction
On a
--with-pydebugbuild (C implementation):(SIGABRT). The pure-Python implementation in
Lib/_pydatetime.pyraisesAssertionErroron the same input. On a release /-DNDEBUGbuild, both implementations correctly raiseValueError: month must be in 1..12, not 0.Root cause
The
24:00midnight-rollover guard validates the upper month bound but not the lower one before callingdays_in_month():Modules/_datetimemodule.c,datetime_datetime_fromisoformat_impl:if ((hour == 24) && (month <= 12))Lib/_pydatetime.py,datetime.fromisoformat:if month <= 12 and day <= (days_in_month := _days_in_month(year, month))The parser does not range-check the month before this point, so
month == 0flows intodays_in_month(year, 0)and hitsassert(month >= 1). The matching upper-bound input"2009-13-01T24:00:00"is already rejected by themonth <= 12check; only the lower bound is missing.Suggested fix
Add the
month >= 1lower bound to the guard in both implementations (mirroringcheck_date_args's existingmonth < 1 || month > 12validation), so the24:00path is only taken for an in-range month and the normalValueErroris raised otherwise. I'll open a PR.This was found while adding a fuzz harness for
fromisoformat.Linked PRs