Fromisoformat Rejects a Trailing Z: Check the Version and Preserve UTC
By ON Lab · Sources checked November 8, 2026
If datetime.fromisoformat rejects a timestamp ending in Z, check the Python version before removing the timezone marker. Current Python supports that UTC form; older versions accepted a narrower set of inputs. A compatibility path can replace only a final Z with +00:00, preserving its offset meaning instead of turning the value into a naive datetime.
Compare the interpreter with its versioned documentation
The original CPython report used Python 3.10.14 and a trailing-Z example. The maintainer pointed to the version difference. Our local comparison used existing Python 3.9.6 and 3.13.18 runtimes, not the reporter’s environment.
import sys
from datetime import datetime
print(sys.version)
print(datetime.fromisoformat("2011-11-04T00:05:23Z"))Python 3.9.6 raised ValueError for that input. Python 3.13.18 parsed an aware datetime whose isoformat output was 2011-11-04T00:05:23+00:00. The current documentation records broader supported forms from Python 3.11. The old report is not evidence of a current parser bug, and this guide is not a recommendation to deploy an unsupported interpreter.
Preserve the final UTC marker in a compatibility path
from datetime import datetime
def parse_timestamp(text):
if text.endswith("Z"):
text = text[:-1] + "+00:00"
result = datetime.fromisoformat(text)
if result.utcoffset() is None:
raise ValueError("An explicit UTC offset is required")
return result
value = parse_timestamp("2011-11-04T00:05:23.123456Z")
assert value.isoformat() == "2011-11-04T00:05:23.123456+00:00"
print(value.isoformat())The suffix-only normalization worked on both tested interpreters, including the fractional-second case. The explicit-offset requirement is an application policy in this example, not a rule imposed by fromisoformat. A timestamp without an offset is rejected rather than silently interpreted as local time or UTC.
Do not remove every Z character or simply slice off the final character of every input. An input already ending in +02:00 needs no such rewrite. The function accepts that offset and retains its meaning. If the application needs UTC storage, convert the aware result deliberately.
A literal Z in strptime does not attach a timezone
from datetime import datetime
text = "2011-11-04T00:05:23Z"
literal = datetime.strptime(text, "%Y-%m-%dT%H:%M:%SZ")
offset = datetime.strptime(text, "%Y-%m-%dT%H:%M:%S%z")
print(literal.isoformat(), literal.utcoffset())
print(offset.isoformat(), offset.utcoffset())On both local runtimes, the literal-Z format produced a naive value with no UTC offset. The %z format produced an aware +00:00 value. Matching the character Z in a format string is not the same as asking the parser to interpret an offset. A fixed format also needs a separate fractional-seconds contract; it is not interchangeable with every accepted ISO input.
Check instants separately from their spelling
The parsed value may serialize with +00:00 rather than Z. That spelling change does not by itself change the instant. In our fixture, 2011-11-04T02:05:23+02:00 represented the same instant as the corresponding 00:05:23 UTC value. Keep a separate original string if exact input spelling is part of an audit requirement.
If the source has no offset, its calendar fields cannot establish a region. The separate local wall-time and timezone guide addresses interpretation and daylight-saving boundaries. Attaching UTC merely to make a comparison run is a different operation from parsing an explicit UTC marker.
Do not mistake successful parsing for full protocol validation
fromisoformat accepts documented forms beyond one strict API timestamp grammar. If an interface requires a particular RFC-style format, validate that contract separately and test both rejected and accepted forms. The compatibility function above is not a complete RFC 3339 validator.
| Observation | Next check |
|---|---|
| Trailing Z fails only on an older runtime | Confirm the supported deployment version and compatibility requirement. |
| An offset-free input parses | Apply the application’s explicit missing-offset policy. |
| Parsing works but the displayed time differs | Inspect the display conversion and intended region separately. |
| Nonstandard input is accepted unexpectedly | Compare it with the protocol’s required grammar. |
Method: current Python 3.13 documentation, a historical Python 3.10 user report and five input variants plus two strptime cases on Python 3.9.6 and 3.13.18/macOS. No original application, every ISO form or strict protocol validator was reproduced.