For Developers

Timezones in Python

A naive datetime and an aware one look almost identical in Python — right up until they're compared, and the mismatch either crashes or, worse, silently returns the wrong answer.

Python's datetime handling has a specific, well-documented trap built into the standard library: a datetime object can look complete and still be "naive" — carrying no timezone information at all — and Python will not warn you when you compare or subtract two naive datetimes that actually represent different zones.

Naive vs. aware, and why the difference is invisible

datetime.now() returns a naive datetime — a date and time with no attached zone, effectively meaning "this could be anywhere." datetime.now(timezone.utc) returns an aware one, which explicitly carries UTC. Both print similarly and both support the same arithmetic, so code written against a naive datetime will run without error for months, until it's compared against an aware one and Python raises TypeError: can't compare offset-naive and offset-aware datetimes — or worse, until two naive datetimes from genuinely different zones get subtracted and produce a plausible-looking but wrong duration, with no error at all.

The fix: be aware everywhere, on purpose

Since Python 3.9, zoneinfo is in the standard library and is the current recommended way to attach a real IANA timezone (like ZoneInfo("America/Chicago")) rather than a bare UTC offset — the same reasoning as in Node: names survive DST changes, fixed offsets don't. Construct datetimes as aware from the moment they're created, store them as aware (or store the UTC value plus the zone name separately), and avoid ever mixing naive and aware values in the same comparison.

For anything beyond straightforward construction and formatting — recurring events, business-day math across zones, DST-aware duration calculations — pendulum and arrow are the two libraries most commonly reached for; both default to timezone-aware objects, which removes the naive/aware footgun by making "aware" the default state rather than something you have to remember to opt into.

A quick way to audit existing code

Search for every datetime.now() and datetime.utcnow() call in a codebase (the latter is deprecated as of Python 3.12 for exactly this reason — it returns a naive datetime that looks like UTC but isn't marked as such) and check whether the result is ever compared against, or stored alongside, a genuinely timezone-aware value elsewhere. That mismatch is the single most common source of the "this timestamp is off by a few hours" class of bug report.

Related

Related tools & reading