Bug report
Bug description:
In the basic ISO 8601 time format, the C implementation of
time.fromisoformat() / datetime.fromisoformat() accepts digits following
HHMMSS and silently interprets them as a fractional seconds component, even
though no decimal mark (. or ,) is present. ISO 8601 requires a decimal
sign to introduce a fraction, and the pure-Python implementation correctly
rejects these strings.
>>> from datetime import time, datetime
>>> time.fromisoformat('12345678')
datetime.time(12, 34, 56, 780000) # expected ValueError
>>> time.fromisoformat('123456789')
datetime.time(12, 34, 56, 789000) # expected ValueError
>>> datetime.fromisoformat('2020-01-01T12345678')
datetime.datetime(2020, 1, 1, 12, 34, 56, 780000) # expected ValueError
The pure-Python implementation rejects all of the above:
>>> import _pydatetime
>>> _pydatetime.time.fromisoformat('12345678')
Traceback (most recent call last):
...
ValueError: Invalid isoformat string: '12345678'
The same parser is used for the UTC offset, so a malformed offset is accepted
too, with the extra digits silently discarded:
>>> time.fromisoformat('12:34:56+00000000')
datetime.time(12, 34, 56, tzinfo=datetime.timezone.utc) # expected ValueError
Two further details show the leniency is unintended rather than deliberate:
- The equivalent extended-format string is correctly rejected —
time.fromisoformat('12:34:5678') raises ValueError.
- The behaviour is not even self-consistent across lengths.
'1234567'
(7 digits) raises ValueError, while '12345678' (8 digits) is accepted.
This matters because the failure is silent and produces a wrong value rather
than an error: a truncated or corrupted timestamp string parses successfully
into a time/datetime that differs from the input.
Cause
In parse_hh_mm_ss_ff() (Modules/_datetimemodule.c), the HH/MM/SS loop
breaks when it sees a decimal mark, but in the basic format it can also fall
out of the loop normally after SS with characters still unconsumed (the
!has_separator branch does --p). The code following the loop then parses
whatever remains as a fraction, without ever checking that a decimal mark
introduced it.
The pure-Python _parse_hh_mm_ss_ff() in Lib/_pydatetime.py performs that
check explicitly:
if pos < len_str:
if tstr[pos] not in '.,':
raise ValueError("Invalid microsecond separator")
Documentation
fromisoformat() is documented to accept "any valid ISO 8601 format" subject to
an explicit list of exceptions; a fraction without a decimal sign is not among
them.
This is the same class of C/pure-Python divergence as gh-152157 (empty fraction
before a timezone designator) and gh-152079.
Affected versions
Reproduced directly on 3.12.3 and on main (3.16.0a0). I did not have 3.13,
3.14 or 3.15 interpreters to hand, but comparing parse_hh_mm_ss_ff() across
the branches shows the cause is present in all of them: the
else if (!has_separator) { --p; } branch and the unguarded "Parse fractional
components" block that follows the loop are unchanged on every active branch.
The 3.14, 3.15 and main copies of the function are identical; the 3.12 and
3.13 copies differ only in other validation paths (decimal mark on
hour/minute, empty fraction, microsecond separator).
CPython versions tested on:
3.12, 3.16, CPython main branch
Operating systems tested on:
Linux
Linked PRs
Bug report
Bug description:
In the basic ISO 8601 time format, the C implementation of
time.fromisoformat()/datetime.fromisoformat()accepts digits followingHHMMSSand silently interprets them as a fractional seconds component, eventhough no decimal mark (
.or,) is present. ISO 8601 requires a decimalsign to introduce a fraction, and the pure-Python implementation correctly
rejects these strings.
The pure-Python implementation rejects all of the above:
The same parser is used for the UTC offset, so a malformed offset is accepted
too, with the extra digits silently discarded:
Two further details show the leniency is unintended rather than deliberate:
time.fromisoformat('12:34:5678')raisesValueError.'1234567'(7 digits) raises
ValueError, while'12345678'(8 digits) is accepted.This matters because the failure is silent and produces a wrong value rather
than an error: a truncated or corrupted timestamp string parses successfully
into a
time/datetimethat differs from the input.Cause
In
parse_hh_mm_ss_ff()(Modules/_datetimemodule.c), theHH/MM/SSloopbreaks when it sees a decimal mark, but in the basic format it can also fallout of the loop normally after
SSwith characters still unconsumed (the!has_separatorbranch does--p). The code following the loop then parseswhatever remains as a fraction, without ever checking that a decimal mark
introduced it.
The pure-Python
_parse_hh_mm_ss_ff()inLib/_pydatetime.pyperforms thatcheck explicitly:
Documentation
fromisoformat()is documented to accept "any valid ISO 8601 format" subject toan explicit list of exceptions; a fraction without a decimal sign is not among
them.
This is the same class of C/pure-Python divergence as gh-152157 (empty fraction
before a timezone designator) and gh-152079.
Affected versions
Reproduced directly on 3.12.3 and on
main(3.16.0a0). I did not have 3.13,3.14 or 3.15 interpreters to hand, but comparing
parse_hh_mm_ss_ff()acrossthe branches shows the cause is present in all of them: the
else if (!has_separator) { --p; }branch and the unguarded "Parse fractionalcomponents" block that follows the loop are unchanged on every active branch.
The 3.14, 3.15 and
maincopies of the function are identical; the 3.12 and3.13 copies differ only in other validation paths (decimal mark on
hour/minute, empty fraction, microsecond separator).
CPython versions tested on:
3.12, 3.16, CPython main branch
Operating systems tested on:
Linux
Linked PRs