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.
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.
datetime.fromisoformat (the modern fast path)
datetime.fromisoformat parses ISO 8601 strings without any format argument. The accepted inputs:
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 source
Use
Why
ISO 8601 API response (JSON timestamps)
fromisoformat
Fastest, no format spec, standards-compliant
Database datetime column (ISO format)
fromisoformat
Modern DB drivers already return datetime objects, but raw text uses ISO
Custom log file with known format
strptime
You control the format; spec it explicitly
Email date headers
email.utils.parsedate_to_datetime
RFC 2822 has its own quirks; use the dedicated parser
User-supplied date input
dateutil.parser.parse
Users type whatever; auto-detect is the right default
Multi-region data (mixed formats)
dateutil.parser with dayfirst hint
Tell it the regional convention
Performance-critical hot loop
fromisoformat or strptime
Both 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.
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).
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.
GO TO FULL VERSION