[EDIT: ignore this message, see next one.
I wrote this whole message noticing mild strptime
improvements but before I realized fromisoformat
is much improved since 3.11.
]
Turns out datetime.datetime.strptime()
does accept Z
suffix when parsing %z
specifier
:
Changed in version 3.7: When the %z
directive is provided to the strptime()
method, the UTC offsets can have a colon as a separator between hours, minutes and seconds. For example, '+01:00:00'
will be parsed as an offset of one hour. In addition, providing 'Z'
is identical to '+00:00'
.
This emits tz-aware object, using timedelta
, so achieves the important goal of reading an unambiguous moment in time, with stdlib, without packaging a TZ database 
>>> datetime.datetime.strptime('2024-10-12t06:29:22.1z'.upper(), '%Y-%m-%dT%H:%M:%S.%f%z')
datetime.datetime(2024, 10, 12, 6, 29, 22, 100000, tzinfo=datetime.timezone.utc)
>>> datetime.datetime.strptime('2024-10-12T06:29:22.12345+0030', '%Y-%m-%dT%H:%M:%S.%f%z')
datetime.datetime(2024, 10, 12, 6, 29, 22, 123450, tzinfo=datetime.timezone(datetime.timedelta(seconds=1800)))
So I’ll hijack this thread for a more modest goal — not touching fromisoformat()
, just having ingredients in stdlib for reasonably parsing RFC3339.
- Non-goal: raising strict errors for every deviation from the RFC.
Incomplete state of datetime.strptime (testing on 3.13.0, linux, glibc-2.39-22):
[I’m not talking of time.strptime
which is slightly different and less suitable]
dateTtime separator & case
NOTE: Per [ABNF] and ISO8601, the “T” and “Z” characters in this syntax may alternatively be lower case “t” or “z” respectively.
This date/time format may be used in some environments or contexts that distinguish between the upper- and lower-case letters ‘A’-‘Z’ and ‘a’-‘z’ (e.g. XML).
Specifications that use this format in such environments MAY further limit the date/time syntax so that the letters ‘T’ and ‘Z’ used in the date/time syntax must always be upper case.
Applications that generate this format SHOULD use upper case letters.
NOTE: ISO 8601 defines date and time separated by “T”.
Applications using this syntax may choose, for the sake of readability, to specify a full-date and full-time separated by (say) a space character.
-
strptime T
does match lowercase t
as well (or vice versa).
-
strptime T
will not match a space or any other separator.
[Unlike fromisoformat
which accepts any character whatsoever.]
The RFC seems vague whether a space or other separators are conformant??
Would one know when specifically the input is e.g. “like rfc3339 only with space”, or SHOULD one accept any character whenever they parse rfc timestamps? If the latter, there is no easy way to use strptime
? Well maybe slicе it 
datetime.datetime.strptime(s[:10] + 'T' + s[11:], '%Y-%m-%dt%H:%M:%S.%f%z')
-
strptime %z
accepts uppercase Z
but refuses to parse lowercase z
. Easy enough to call .upper()
first but would be nice if %z accepted both…
Fractional seconds
RFC says either omit fraction, or include 1+ digits, no limit.
partial-time = time-hour ":" time-minute ":" time-second [time-secfrac]
time-secfrac = "." 1*DIGIT # 1* is ABNF for 1 or more
Well by now (Python 3.13.0) I see fromisoformat()
accepts anything from no fraction, 1 to 6 digits (those are parsed), as well as ANY higher number of digits (9 or 10000…) of which only first 6 are kept, datetime having µsec resolution 
strptime()
's %f OTOH is lacking:
-
.%f
accepts 1–6 fractional seconds.
-
.%f
refuses to parse (“does not match format”) date without fractional seconds. Neither ...T06:29:22Z
(RFC compliant) nor ...T06:29:22.Z
(invalid) work.
=> You must retry with and without .%f
.
Annoying, but kinda natural with the way formats work, even if %f
accepted 0 digits, literal .
still must match a period?
-
.%f
refuses to parse fraction with >6 digits.
This is the worst gap IMO because optional variable-length fractions come before %z of also varying length, so are hard to trim.
Well, re.sub(r'\.([0-9]{6})[0-9]+', r'.\1', s)
keeps first 6 digits. 
TZ offset parsing is good
- %z can parse all forms RFC allows except lowercase
z
: Z
+01:23
, -23:45
.
- %z can also parse seconds
+01:23:45
, without colons -0123
, +012345
and even fractions 2024-10-12T06:29:22.1+01:23:45.000678
(afaik nobody needs those, it’s just supported because timedelta
has µsec resolution for other uses).
Naive objects
strftime %z is valid for naive objects, emits empty string — but strptime %z refuses to parse them. If you care you’d have to retry without %z.
But that’s OK
, deliberately out of scope of RFC 3339:
Since interpretation of an unqualified local time zone will fail in approximately 23/24 of the globe, the interoperability problems of unqualified local time are deemed unacceptable for the Internet.
Leap seconds
Unparsable, but datetime currently can’t represent them anyway 
>>> datetime.datetime.strptime('2005-12-31T23:59:60Z', '%Y-%m-%dT%H:%M:%S%z')
Traceback (most recent call last):
File "<python-input-188>", line 1, in <module>
datetime.datetime.strptime('2005-12-31T23:59:60Z', '%Y-%m-%dT%H:%M:%S%z')
~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib64/python3.13/_strptime.py", line 584, in _strptime_datetime
return cls(*args)
ValueError: second must be in 0..59
>>> datetime.datetime(2005, 12, 31, 23, 59, 60, tzinfo=datetime.timezone.utc)
Traceback (most recent call last):
File "<python-input-185>", line 1, in <module>
datetime.datetime(2005, 12, 31, 23, 59, 60, tzinfo=datetime.timezone.utc)
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: second must be in 0..59
Unknown offset out of scope IMHO?
- Section 4.3. Unknown Local Offset Convention suggests using
-00:00
to mean “same time moment as +00:00
/Z
but unknown where on earth”. datetime
today just has no way to represent this subtlety. We MUST use a TZ-aware object either way. So nothing to do. 
Summary wishlist
[EDIT: with 3.11 improvements fromisoformat(s.upper())
is nicer than any strptime
approximation. The following are YAGNI]
But as discussed fromisoformat
is bound by other goals, whereas these sound harmless to me:
datetime.strptime
%z to accept lowercase z
. 
datetime.strptime
%f to accept and discard >6 digits. 
Or would these be considered breaking compatibility for “strict” users relying on these raising errors? 
That still leaves the nuisance of seconds .fraction being optional:
try:
d = datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%f%z')
except ValueError:
d = datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%S%z')
In a green-field world I’d suggest making up some notation e.g. %.f
…
But AFAICT no current % directive can match empty string, it’d introduces back-tracking issues?
Also, the % space is very crowded, with both C standard and de-facto libc implementations growing in future, need to think twice before adding our owns(?)
- Well WDYT of strptime taking tuple of formats and trying them in order?

RFC3339_FORMATS = ('%Y-%m-%dT%H:%M:%S.%f%z', '%Y-%m-%dT%H:%M:%S%z')
d = datetime.strptime(s, RFC3339_FORMATS)
Is all this better than adding a tailored fromrfc3339
method, or using existing external packages?
For stdlib, IMHO yes — same improvements could help parse other reasonable time formats.
For example “like 3339 but with space”, other subsets of 8601, English style with 12h am/pm but long fractional seconds, etc. …