Handling Time Zones and DST in Python: zoneinfo, pytz, and datetime
Python's date handling has a genuinely confusing history — three different ways to represent a time zone have been in common use depending on which Python version and which library a given piece of code was written against. Here's what actually works today, and why.
1. Naive vs. Aware: The Distinction That Matters Most
Every datetime object in Python is either "naive" (no time zone attached —
Python has no idea what zone it represents) or "aware" (it carries an explicit
tzinfo). This is the single most important distinction in Python's date
handling: comparing or subtracting a naive datetime and an aware one raises a
TypeError, and code that silently mixes the two is one of the most common
sources of Python timezone bugs.
from datetime import datetime
naive = datetime(2026, 7, 22, 14, 30) # no time zone info at all
aware = datetime(2026, 7, 22, 14, 30, tzinfo=None) # still naive — tzinfo=None isn't a zone2. zoneinfo: The Modern Standard Library Way (Python 3.9+)
As of Python 3.9, the standard library ships zoneinfo, which reads time
zone rules straight from the IANA Time Zone Database already installed on the
system — the same database this whole site is built on — no third-party package
required.
from datetime import datetime
from zoneinfo import ZoneInfo
now_tokyo = datetime.now(ZoneInfo("Asia/Tokyo"))
print(now_tokyo)
# 2026-07-22 23:30:00+09:003. Why pytz Is on Its Way Out
Before zoneinfo existed, pytz was the de facto standard, but
it has a well-known sharp edge: you can't just pass a pytz timezone
directly to datetime()'s tzinfo argument the way you can with
zoneinfo — doing so silently produces the wrong offset for zones with more
than one historical UTC offset. The correct (but easy to forget) pattern requires
calling .localize() instead:
import pytz
from datetime import datetime
# WRONG — silently uses the zone's very first historical offset, not the current one
wrong = datetime(2026, 7, 22, 14, 30, tzinfo=pytz.timezone("America/New_York"))
# CORRECT — pytz's own localize() computes the right offset for this date
tz = pytz.timezone("America/New_York")
right = tz.localize(datetime(2026, 7, 22, 14, 30))
zoneinfo doesn't have this footgun — datetime(...,
tzinfo=ZoneInfo(...)) just works correctly — which is the main reason the Python
core team added it and now recommends it over pytz for anything running
Python 3.9+.
4. Converting Between Zones
Once a datetime is aware, converting it to another zone is a single call to .astimezone():
from zoneinfo import ZoneInfo
ny_time = datetime.now(ZoneInfo("America/New_York"))
tokyo_time = ny_time.astimezone(ZoneInfo("Asia/Tokyo"))5. Detecting Whether a Zone Observes DST
The same January-vs-July offset comparison used throughout DateTimeX (see the Handling Timezones & UTC in JavaScript guide for the JavaScript version) works identically in Python:
from datetime import datetime
from zoneinfo import ZoneInfo
tz = ZoneInfo("Europe/London")
jan_offset = datetime(2026, 1, 15, tzinfo=tz).utcoffset()
jul_offset = datetime(2026, 7, 15, tzinfo=tz).utcoffset()
observes_dst = jan_offset != jul_offset # True for London, False for Tokyo6. Common Pitfalls
datetime.utcnow()returns a naive datetime, despite the name — it gives you the current UTC time as a value with no time zone attached, which invites exactly the naive/aware mixing bug from section 1. Preferdatetime.now(ZoneInfo("UTC")), which is aware.datetime.now()with no argument uses the system's local time zone as a naive result — fine for quick scripts, risky in server code where "local" means the server's zone, not any user's.- Windows systems may not ship the IANA time zone database by default,
so
zoneinfoon Windows can raiseZoneInfoNotFoundErrorunless thetzdatapackage (pip install tzdata) is installed — Linux and macOS almost always have it preinstalled at the OS level.
Related
The same core techniques — offset lookup, DST detection, zone conversion — are covered for JavaScript in Handling Timezones & UTC in JavaScript. To sanity-check a specific conversion by eye, use our DST Tracker.