Parsing a datetime in Python has three canonical answers and one rule. Pick by where the input comes from. datetime.fromisoformat() is the modern fast path for ISO 8601 strings (the standard that nearly all APIs ship in 2026); Python 3.11 made it accept the full standard including the Z suffix and offsets. datetime.strptime() is the workhorse for known custom formats with format codes (%Y-%m-%d, %H:%M:%S). dateutil.parser.parse() is the third-party flexible parser for inputs whose format you can't control. The rule: store datetimes in UTC, use timezone-aware objects everywhere, and convert to local time only at the display boundary. This entry covers all three methods, the format-code reference, the Z-suffix workaround for legacy Python, and the edge cases that surface in production. Part of our Stdlib Essentials guide.

Key Takeaways

  • datetime.fromisoformat(s) is the canonical 2026 answer for ISO 8601 input; Python 3.11+ handles the full standard including Z.
  • datetime.strptime(s, fmt) parses known custom formats via format codes; slower than fromisoformat but handles anything you can describe.
  • dateutil.parser.parse(s) auto-detects the format; useful for messy inputs but ambiguous dates can misparse.
  • Always parse to timezone-aware datetimes; mixing naive and aware objects raises TypeError on comparison.
  • Pre-3.11 Z-suffix workaround: fromisoformat(s.replace('Z', '+00:00')).
3 parsing methods, 4 input formats, when each works 3 datetime parsers, 4 input formats, which works when FROMISOFORMAT · STRPTIME · DATEUTIL.PARSER INPUT STRING fromisoformat() stdlib, fastest, ISO 8601 strptime() stdlib, format codes dateutil.parser.parse() 3rd-party, auto-detect ISO with Z 2026-06-25T14:30:00Z canonical API format ✓ Python 3.11+ 3.10 and older: replace Z with +00:00 first ~ works with effort need %Y-%m-%dT%H:%M:%S%z and Z-to-offset hack ✓ works directly handles Z transparently on every Python Date only 2026-06-25 CSV, query params ✓ all versions ✓ with %Y-%m-%d need exact format ✓ works directly Log-style Jun 25, 2026 2:30 PM human-readable logs ✗ not ISO format ✓ with custom fmt %b %d, %Y %I:%M %p ✓ auto-detected Ambiguous 06/07/2026 US vs European order ✗ not ISO format ~ pick format force MM/DD or DD/MM ~ guesses (often wrong) defaults US: June 7 STRPTIME FORMAT CODES (most common) %Y4-digit year (2026) %mmonth as number (06) %dday as number (25) %Hhour 00-23 %Mminute 00-59 %Ssecond 00-59 %Ihour 12-format 01-12 %pAM or PM %zUTC offset +HHMM %bmonth abbr (Jun) %Bmonth full (June) %aweekday abbr (Thu) %jday of year 001-366 %Uweek number 00-53 %%literal % character Full reference: docs.python.org/3/library/datetime.html#strftime-strptime-behavior DECISION RULE ISO 8601? → fromisoformat. Known custom format? → strptime. Unknown / messy input? → dateutil.parser.
Three methods, four input formats, when each works. Bottom panel: the strptime format codes you'll use most often.

The Canonical 2026 Answer

For nearly every modern API and interchange format:
from datetime import datetime

# ISO 8601 with timezone (Python 3.11+)
dt = datetime.fromisoformat("2026-06-25T14:30:00Z")
# datetime(2026, 6, 25, 14, 30, tzinfo=timezone.utc)
That's the whole story for the common case. ISO 8601 is the de facto interchange format for JSON APIs, log files, and database exports. fromisoformat is fast (no format-string parsing overhead) and handles the standard correctly.

For Python 3.10 and older, you need to replace the Z first:
# Pre-3.11 workaround for Z suffix
s = "2026-06-25T14:30:00Z"
dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
This is the most common production gotcha. Modern code on 3.11+ skips it; legacy code targeting older Python keeps it as a one-liner.Convert String to datetime in Python: ISO 8601, strptime, and dateutil - 1

datetime.fromisoformat (the modern fast path)

datetime.fromisoformat parses ISO 8601 strings without any format argument. The accepted inputs:
from datetime import datetime

datetime.fromisoformat("2026-06-25")
# datetime(2026, 6, 25, 0, 0)

datetime.fromisoformat("2026-06-25T14:30:00")
# datetime(2026, 6, 25, 14, 30)

datetime.fromisoformat("2026-06-25T14:30:00+02:00")
# datetime(2026, 6, 25, 14, 30, tzinfo=timezone(timedelta(hours=2)))

# Python 3.11+ only:
datetime.fromisoformat("2026-06-25T14:30:00Z")
datetime.fromisoformat("20260625T143000")          # compact ISO
datetime.fromisoformat("2026-06-25 14:30:00.123")   # microseconds
For new code in 2026 on Python 3.11+, this is your default. It's strict (rejects malformed inputs cleanly) and fast (no format-string interpretation).

datetime.strptime (the format-code workhorse)

For custom formats that aren't ISO 8601 (log file timestamps, user-facing dates, legacy data), use datetime.strptime with a format string.
from datetime import datetime

# Log file format
datetime.strptime("Jun 25 2026 14:30:00", "%b %d %Y %H:%M:%S")
# datetime(2026, 6, 25, 14, 30)

# US date format
datetime.strptime("06/25/2026 02:30 PM", "%m/%d/%Y %I:%M %p")
# datetime(2026, 6, 25, 14, 30)

# With timezone offset
datetime.strptime("2026-06-25T14:30:00+0200", "%Y-%m-%dT%H:%M:%S%z")
# datetime(2026, 6, 25, 14, 30, tzinfo=timezone(timedelta(hours=2)))
The format string uses the codes documented in the PSF format-code reference. The visual above lists the most common ones; the docs cover the full set.

dateutil.parser (the auto-detector)

The third-party python-dateutil package adds a flexible parser that detects the format from the string itself.
from dateutil.parser import parse

parse("2026-06-25T14:30:00Z")            # works on any Python version
parse("Jun 25, 2026 2:30 PM")             # auto-detects format
parse("June 25th, 2026 at 2:30pm")        # handles colloquial input
parse("06/07/2026")                        # AMBIGUOUS - defaults to US convention
parse("06/07/2026", dayfirst=True)        # force day-first interpretation
For inputs you can't standardize (user-supplied dates, log files in unknown formats, data from many regions), dateutil is the right tool. The trade-offs:
  • Third-party dependency: install with pip install python-dateutil; many Python apps already have it transitively.
  • Slower than stdlib: auto-detection has real cost; for hot loops, prefer explicit parsing.
  • Ambiguous inputs misparse: 06/07/2026 could be June 7 or July 6; dateutil defaults to US (month first). Use dayfirst=True for European, or normalize the input format upstream.

The Decision Matrix

Input sourceUseWhy
ISO 8601 API response (JSON timestamps)fromisoformatFastest, no format spec, standards-compliant
Database datetime column (ISO format)fromisoformatModern DB drivers already return datetime objects, but raw text uses ISO
Custom log file with known formatstrptimeYou control the format; spec it explicitly
Email date headersemail.utils.parsedate_to_datetimeRFC 2822 has its own quirks; use the dedicated parser
User-supplied date inputdateutil.parser.parseUsers type whatever; auto-detect is the right default
Multi-region data (mixed formats)dateutil.parser with dayfirst hintTell it the regional convention
Performance-critical hot loopfromisoformat or strptimeBoth faster than dateutil; pick by format

Timezone Handling: Always Aware

Python distinguishes naive datetimes (no tzinfo) from aware datetimes (with tzinfo). The 2026 best practice: use aware datetimes everywhere, store and process in UTC, convert to local time only at display boundaries.
from datetime import datetime, timezone, timedelta

# Naive: no timezone, ambiguous about wall-clock vs UTC
naive = datetime.fromisoformat("2026-06-25T14:30:00")
print(naive.tzinfo)        # None

# Aware: explicit timezone
aware_utc = datetime.fromisoformat("2026-06-25T14:30:00+00:00")
aware_local = datetime.fromisoformat("2026-06-25T14:30:00-04:00")

# Convert to a different timezone
ny = aware_utc.astimezone(timezone(timedelta(hours=-5)))
The dangerous mistake: mixing naive and aware in the same code path. Comparing them raises TypeError, which is good (it surfaces the bug) but inconvenient when bugs are in a hurry.
naive = datetime(2026, 6, 25, 14, 30)
aware = datetime(2026, 6, 25, 14, 30, tzinfo=timezone.utc)

naive == aware
# TypeError: can't compare offset-naive and offset-aware datetimes
To force a naive datetime to a known timezone, attach the tzinfo:
aware = naive.replace(tzinfo=timezone.utc)
This is correct ONLY if you genuinely know the naive datetime was UTC. For zone-aware data (using zoneinfo from Python 3.9+):
from zoneinfo import ZoneInfo

aware = naive.replace(tzinfo=ZoneInfo("America/New_York"))
The zoneinfo module is the modern way to handle named time zones; it replaces the older pytz library for most uses.

Edge Cases

The 24:00 problem

ISO 8601 allows 24:00:00 as a valid representation of midnight at the end of the day. Python's fromisoformat accepts 00:00:00 only. If your input might use 24:00:00, normalize it first:
s = "2026-06-25T24:00:00"
if "T24:00:00" in s:
    s = s.replace("T24:00:00", "T00:00:00")
    # then add one day after parsing

Leap seconds

ISO 8601 allows 23:59:60 for the rare leap second. Python's datetime module rejects it; second values are 0-59 only. For systems that genuinely care about leap seconds (astronomy, GPS), use an external library or pre-process.

Two-digit years

strptime with %y interprets two-digit years using a fixed cutoff (years 00-68 are 2000s, 69-99 are 1900s in current implementations). For two-digit years from data older than the 1960s, parse and adjust manually.

Real-World Patterns

JSON API response with ISO timestamps

import json
from datetime import datetime

data = json.loads(response.text)
created_at = datetime.fromisoformat(data["created_at"])
# Already UTC-aware if the API used Z or +HH:MM
The clean modern path. If the API drops timezone info entirely, attach UTC explicitly with .replace(tzinfo=timezone.utc).

Log file parsing

def parse_log_timestamp(line: str) -> datetime:
    # Log format: "[2026-06-25 14:30:00.123] INFO ..."
    ts = line[1:24]    # extract the bracket contents
    return datetime.strptime(ts, "%Y-%m-%d %H:%M:%S.%f").replace(tzinfo=timezone.utc)
Known fixed format, fast strptime, attach UTC because logs are typically wall-clock UTC.

Multi-format user input

from dateutil.parser import parse, ParserError

def parse_user_date(s: str) -> datetime | None:
    try:
        return parse(s, dayfirst=False)   # US convention
    except (ParserError, ValueError):
        return None
Accepts colloquial inputs; returns None on failure for the caller to handle.

Unix Timestamps

For numeric Unix timestamps (seconds or milliseconds since 1970-01-01 UTC), the path is datetime.fromtimestamp with explicit timezone:
from datetime import datetime, timezone

# Seconds since epoch (e.g., '1719321600')
ts_seconds = int("1719321600")
dt = datetime.fromtimestamp(ts_seconds, tz=timezone.utc)
# datetime(2026, 6, 25, 12, 0, tzinfo=timezone.utc)

# Milliseconds since epoch (common in JavaScript-flavored APIs)
ts_ms = int("1719321600000")
dt = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc)
Always pass tz=timezone.utc explicitly. Without it, fromtimestamp interprets the timestamp in the local timezone, which is almost never what you want from numeric epoch data. The tz argument has been available since Python 3.3 and remains the canonical pattern.

Performance: When the Difference Matters

For typical applications (parsing a handful of dates per request), all three methods are fast enough that the difference doesn't matter. For bulk processing (CSV imports, log analysis, ETL pipelines parsing millions of rows), the order is:
  • fromisoformat: fastest, ~5-10x faster than strptime for the same ISO input.
  • strptime: ~10x faster than dateutil for known formats, but slower than fromisoformat because the format string is interpreted at every call.
  • dateutil.parser.parse: slowest, since it has to detect the format from the string for every input.
For hot paths, use fromisoformat when the input is ISO 8601 (which it should be for most modern data). Compile a format-code template once and reuse it with strptime when ISO isn't an option. Reserve dateutil for ad-hoc tools and user input where format auto-detection earns its cost.

Common Mistakes

1. Forgetting the Z suffix on pre-3.11 Python

fromisoformat("2026-06-25T14:30:00Z") raises ValueError on Python 3.10 and older. Use .replace("Z", "+00:00") first.

2. Mixing naive and aware datetimes

Comparing or subtracting a naive datetime with an aware one raises TypeError. Pick one mode (almost always aware) and use it consistently.

3. Trusting dateutil on ambiguous dates

06/07/2026 defaults to June 7. If your users mean July 6, set dayfirst=True or require ISO format upstream.

4. Using strptime when fromisoformat would do

strptime with a format string for ISO inputs is slower and more verbose than fromisoformat. Use the right tool.

5. Storing local time without timezone info

"2026-06-25 14:30" with no tzinfo is ambiguous (DST, which timezone). Convert to UTC at the boundary and store aware datetimes.

Frequently Asked Questions

How do I convert a string to a datetime in Python?

Three canonical methods. For ISO 8601 strings (the recommended interchange format), use datetime.fromisoformat(s). Python 3.11+ accepts the full ISO 8601 standard including Z suffix and offsets. For known custom formats like '06/25/2026 14:30', use datetime.strptime(s, '%m/%d/%Y %H:%M'). For ambiguous inputs from many sources, install python-dateutil and use dateutil.parser.parse(s), which attempts to detect the format automatically. Prefer fromisoformat for new code shipping ISO 8601 between systems; reach for strptime when you control the format; reserve dateutil for inputs you can't standardize.

What is the difference between fromisoformat and strptime in Python?

fromisoformat parses ISO 8601 strings without needing a format argument; the parser is hard-coded for the ISO format and is fast. strptime parses any format you describe with format codes (%Y-%m-%d for year-month-day, etc.); it's slower because it has to interpret the format string at runtime, but it handles arbitrary custom formats. Use fromisoformat for ISO 8601 inputs (most modern APIs); use strptime for legacy formats, locale-specific outputs, or anything that isn't ISO. Python 3.11 expanded fromisoformat to accept the full ISO standard, making it the right choice for nearly all interchange data.

How do I handle the Z suffix in ISO 8601 strings in Python?

Z is the ISO 8601 designation for UTC time (Zulu time). On Python 3.11+, datetime.fromisoformat('2026-06-25T14:30:00Z') works directly. On Python 3.7-3.10, fromisoformat doesn't accept Z and you need to replace it: datetime.fromisoformat(s.replace('Z', '+00:00')). The replace pattern is the canonical workaround for legacy Python. Alternatively, dateutil.parser handles Z transparently on all Python versions if you can add the dependency.

Should I use naive or timezone-aware datetimes in Python?

Use timezone-aware datetimes for nearly all code that processes real-world dates. Naive datetimes (no tzinfo attached) work for trivial local-time arithmetic but cause bugs whenever data crosses timezones, DST changes happen, or two systems share datetimes. The 2026 best practice: store datetimes in UTC, attach tzinfo=timezone.utc to every datetime you create or parse, and convert to local time only at display boundaries. Mixing naive and aware datetimes raises TypeError when you compare them, which usually surfaces production bugs quickly but inconvenient bugs in a hurry.

When should I install python-dateutil instead of using stdlib?

Install python-dateutil when your input format is unknown or varies. The stdlib's fromisoformat and strptime are strict; both require knowing the exact format. dateutil.parser.parse attempts to detect the format from the string, handles many regional variations (US vs European date order), and accepts colloquial inputs like 'next Tuesday' or 'June 25 at 2pm'. The trade-offs: dateutil is a third-party dependency, the auto-detection can misparse ambiguous inputs (06/07/2026 could be June 7 or July 6 depending on locale), and the parsing is slower. For known formats stick with stdlib; for messy inputs reach for dateutil.

The Bottom Line: fromisoformat for ISO, strptime for known formats, dateutil for the rest

Three parsing methods, one decision rule. Pick by input source: ISO 8601 wires up to fromisoformat (the modern fast path); known custom formats go through strptime with format codes; unknown or messy inputs warrant dateutil.parser. Always parse to timezone-aware datetimes; store in UTC; convert to local time only at display boundaries. Use the Z-to-offset replace trick for pre-3.11 Python. Next: raise, raise from, and re-raising exceptions. Or browse the full Stdlib Essentials guide.

Build Datetime Reflexes Through Practice

CodeGym's Python track turns datetime parsing into reflex across 62 levels: ISO 8601 via fromisoformat, strptime for legacy formats, dateutil for messy inputs, timezone-aware patterns. AI validators catch the naive-aware mixing bugs and the Z-suffix oversight; AI mentors explain why a particular format string failed. First level free; full plan on the pricing page.