How do I work with time zones in Python using zoneinfo?
Short answer
Attach a real zone to a datetime with ZoneInfo, then convert with astimezone. zoneinfo is built into Python 3.9 and later and handles daylight saving, unlike a fixed offset.
The short version
Use zoneinfo.ZoneInfo to attach a real, named time zone to a datetime, then move between zones with astimezone. zoneinfo ships with Python 3.9 and later and understands daylight saving from the system time zone database, which a fixed timedelta offset does not. Store instants in UTC and convert to local zones only for display.
Attaching a zone and converting
from datetime import datetime
from zoneinfo import ZoneInfo
london = datetime(2021, 6, 1, 12, tzinfo=ZoneInfo("Europe/London"))
new_york = london.astimezone(ZoneInfo("America/New_York"))
print(london) # 2021-06-01 12:00:00+01:00
print(new_york) # 2021-06-01 07:00:00-04:00
Setting tzinfo labels the datetime with a zone; astimezone shifts it to the equivalent moment in another zone. The offsets shown (+01:00, -04:00) reflect summer time, which zoneinfo applies automatically.
Naive versus aware datetimes
A datetime with no tzinfo is naive: it has no zone, so it cannot be converted and comparisons across zones are meaningless. One with a zone is aware. Mixing the two raises, which is a common error.
naive = datetime(2021, 6, 1, 12)
naive.astimezone(ZoneInfo("America/New_York")) # assumes local zone, better to be explicit
Make datetimes aware as early as possible, at the point data enters your program, so the rest of the code deals only with aware values.
Why not a fixed offset
Writing timezone(timedelta(hours=1)) for London is wrong for half the year, because it ignores daylight saving. A named zone like Europe/London knows when the clocks change, so it gives the correct offset for any date. Always use a named zone rather than a hard-coded offset for real locations.
The UTC discipline
The robust pattern is to store and compute in UTC, and convert to a local zone only when showing a time to a person.
now = datetime.now(ZoneInfo("UTC"))
now.astimezone(ZoneInfo("Europe/London")) # for display
Keeping the internal representation in one zone avoids a whole family of off-by-an-hour bugs, because arithmetic and comparisons all happen in a single, unambiguous frame.
The daylight saving edge cases
When the clocks change, some local times do not exist and others happen twice. zoneinfo resolves these with the fold attribute for the repeated hour, but the safest defence is to do arithmetic in UTC and convert only at the edges, so you never do maths across a clock change in local time.
Availability and data
zoneinfo is standard from Python 3.9. On some minimal systems the time zone database may be missing, in which case installing the tzdata package provides it. On older Python versions, the third-party pytz library filled this role, but zoneinfo is now the recommended built-in choice.