skip to content

How do datetime.fromisoformat() and datetime.strptime() differ when parsing a timestamp string?

level: juniorimportance: should knowfreq 50%

answer

  1. One parser needs no format string
  2. The other is directive-driven and general
  3. A version bump widened what ISO text is accepted
  4. Month names depend on the process locale
  5. Pair the emitter with its matching reader

basics

~20 s

fromisoformat parses ISO 8601 text with no format string, in C, and never touches the locale. strptime takes an explicit directive format so it reads any layout, but it is slower and locale-sensitive for month and weekday names.

solid answer

~40 s

`datetime.fromisoformat()` takes only the string: it parses ISO 8601 text, is implemented in C, and involves no locale at all. Since Python 3.11 it accepts most valid ISO 8601 forms, including a trailing `Z` and the compact basic form `20260904T101112`; before 3.11 it only accepted what `isoformat()` itself emitted. `datetime.strptime()` takes the string plus a format built from directives such as `%Y`, `%d`, `%b`, `%H` and `%z`, so it can read arbitrary layouts like an access-log stamp - but it is markedly slower, requires the whole string to match, and `%a`, `%b` and `%p` depend on the process locale. The rule of thumb: emit with `isoformat()`, read with `fromisoformat()`, and reach for `strptime()` only for formats you do not control.

code

python · 6 lines
python
from datetime import datetime, timezone

print(datetime.fromisoformat("2026-09-04T10:11:12Z"))
print(datetime.fromisoformat("20260904T101112"))
print(datetime.strptime("04/Sep/2026:10:11:12 +0000", "%d/%b/%Y:%H:%M:%S %z"))
print(datetime.now(timezone.utc).isoformat(timespec="seconds"))

go deeper

for a junior

Know that fromisoformat needs no format string and strptime does, and be able to name the common directives %Y, %m, %d, %H, %M, %S. Recall that isoformat and fromisoformat are the matching pair.

for a middle

Explain the 3.11 widening of fromisoformat, why the old Z workaround existed, and what strptime's %z gives you that %Z does not. Be able to say why strptime is the slower, locale-sensitive option.

for a senior

Show the discipline: ISO 8601 with an explicit offset on the wire, a pinned timespec for fixed-width sortable stamps, strptime confined to formats you do not control, and locale-dependent directives kept out of machine-readable data.

for a principal

Own the serialisation contract across services: one timestamp representation, documented precision, and an explicit rule for whether the offset is rendered as +00:00 or Z. Format drift between producers is the expensive class of bug here, not parser choice.

## Two parsers with different contracts The standard library ships two ways to turn text into a `datetime`, and they are aimed at different jobs. `datetime.fromisoformat(s)` takes one argument. It parses ISO 8601 date-time text and nothing else. It is implemented in C, so it is roughly an order of magnitude faster than the alternative, and it never consults the process locale. `datetime.strptime(s, fmt)` takes the string plus a format string made of directives. It is driven by a pure-Python helper that builds a regular expression from the format, so it is slower, and several directives are locale-dependent by design. ## What fromisoformat accepts This is the part with a version story, and interviewers do ask for it. **Before Python 3.11**, `fromisoformat()` was documented as the inverse of `isoformat()` and little more: it accepted the exact shapes `isoformat()` emits and rejected almost everything else - famously including a trailing `Z`, which is the single most common ISO timestamp on the wire. Code from that era is full of `s.replace("Z", "+00:00")`. **Python 3.11 widened it** to accept most valid ISO 8601 forms: ```python from datetime import datetime datetime.fromisoformat("2026-09-04T10:11:12Z") # trailing Z, aware, UTC datetime.fromisoformat("2026-09-04T10:11:12+02:00") # explicit offset datetime.fromisoformat("20260904T101112") # basic (no separators) datetime.fromisoformat("2026-09-04 10:11:12") # space separator ``` A few ISO forms remain unsupported - fractional hours and minutes, for instance - so it is "most of ISO 8601", not all of it. `date.fromisoformat()` and `time.fromisoformat()` are the matching parsers for the other two types. On failure it raises `ValueError`. It does not partially parse: either the whole string is a recognised ISO form or you get an exception. ## What strptime buys you `strptime()` is the general tool. It reads formats nobody would call ISO: ```python from datetime import datetime datetime.strptime("04/Sep/2026:10:11:12 +0000", "%d/%b/%Y:%H:%M:%S %z") datetime.strptime("Sep 4 2026 10:11AM", "%b %d %Y %I:%M%p") ``` Directives worth having at your fingertips: `%Y` four-digit year, `%m` month number, `%d` day, `%H` hour on a 24-hour clock, `%I` with `%p` for a 12-hour clock, `%M`, `%S`, `%f` for one to six fractional-second digits, `%b`/`%B` abbreviated and full month name, `%a`/`%A` weekday name, `%j` day of year, `%%` a literal percent. `%z` parses a UTC offset - `+0000`, `+00:00`, and since Python 3.7 also a bare `Z` - and produces an *aware* result. `%Z` matches a zone *name*, but only a small set that the platform recognises, and it cannot in general reconstruct a real zone, so parsing `%Z` is a trap rather than a feature. Three things that bite: 1. **The match must be total.** Leftover text raises `ValueError: unconverted data remains: ...`, and a format longer than the input raises a mismatch error. That strictness is good - it turns a silently wrong parse into an exception. 2. **Locale sensitivity.** `%a`, `%A`, `%b`, `%B` and `%p` match whatever the current `LC_TIME` locale calls those things. A service that calls `locale.setlocale()` at start-up, or runs on a host with a different locale, will suddenly fail to parse `Sep`. Machine-readable timestamps should never round-trip through a locale-dependent directive. 3. **Cost.** Format compilation is cached but the parse still runs Python-level code. Parsing millions of log lines is where `fromisoformat()`'s C implementation shows up plainly in a profile. ## The round trip you should default to For data you own on both ends, pair `isoformat()` with `fromisoformat()`: ```python from datetime import datetime, timezone stamp = datetime.now(timezone.utc).isoformat(timespec="seconds") # '2026-09-04T10:11:12+00:00' back = datetime.fromisoformat(stamp) ``` `isoformat()` takes `sep` (default `'T'`) and `timespec`, which selects how much of the time is rendered: `'auto'`, `'hours'`, `'minutes'`, `'seconds'`, `'milliseconds'` or `'microseconds'`. `'auto'` prints microseconds only when they are non-zero, which produces variable-width output - pinning `timespec` is how you get stable, sortable, fixed-width stamps. One asymmetry to remember: `isoformat()` renders UTC as `+00:00`, never as `Z`. Since 3.11 the parser accepts both, but a strict downstream consumer that demands `Z` needs you to substitute the suffix yourself. ## The interview shape Say which tool you would reach for and why: `fromisoformat()` by default because it is fast, locale-free and needs no format string; `strptime()` when the layout is dictated by someone else. Name the 3.11 change, because the `Z` workaround is still in a lot of live code, and mention that `strptime()`'s locale-dependent directives are the reason machine formats should stay ISO.

  • What does the timespec argument of datetime.isoformat() control, and why pin it?
    It selects how much of the time component is rendered: `'auto'`, `'hours'`, `'minutes'`, `'seconds'`, `'milliseconds'` or `'microseconds'`. The default `'auto'` prints microseconds only when they are non-zero, so stamps vary in width and two logically identical instants can serialise differently. Pinning `timespec='seconds'` or `'milliseconds'` gives fixed-width, lexicographically sortable output and stable cache or idempotency keys.
  • A downstream consumer insists on a trailing Z. How do you produce that from isoformat()?
    `isoformat()` always renders a UTC offset as `+00:00`. Convert to UTC first, then replace the suffix textually - for example `s = dt.astimezone(timezone.utc).isoformat(timespec='seconds').replace('+00:00', 'Z')`. The reverse direction needs no work since Python 3.11, because `fromisoformat()` accepts `Z` directly.
  • Why should a machine-readable log format avoid %b and %a?
    Those directives match month and weekday names in the current LC_TIME locale. A host or process with a different locale writes or expects different text, so the same code that produced a line can fail to parse it elsewhere. Keep numeric, locale-independent directives - or ISO 8601 - for anything a program will read back, and reserve locale-aware formatting for human display.

saying these in an interview costs you the question

  • Thinks fromisoformat still rejects a trailing Z on 3.14
  • Believes strptime tolerates leftover unparsed text
  • Says isoformat emits Z for UTC
  • Claims %Z reliably restores a real time zone
  • Uses strptime for ISO text in a hot parsing loop

context