Time Zones with zoneinfo
18 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)A meeting at 9:00 means different moments in Mumbai and New York. A datetime without a zone is only a clock reading. With a zone, it names one exact moment. Python's zoneinfo module supplies the zones.
Naive and aware datetimes
Aware datetime is a datetime that has a time zone attached, so it names one exact moment. A naive datetime has no zone and is only a reading on a clock.
A naive datetime has no zone, so it is only a reading on a clock. An aware datetime carries a zone, so it names one exact moment. Python refuses to compare a naive with an aware one.
from datetime import datetime, timezone
naive = datetime(2026, 11, 8, 9, 0)aware = datetime(2026, 11, 8, 9, 0, tzinfo=timezone.utc)print(naive.tzinfo, aware.tzinfo)Example 1 · Naive versus aware
from datetime import datetime, timezone
naive = datetime(2026, 11, 8, 9, 0)aware = datetime(2026, 11, 8, 9, 0, tzinfo=timezone.utc)print(naive.tzinfo, aware.tzinfo)try: print(naive < aware)except TypeError as err: print("TypeError:", err)None UTC TypeError: can't compare offset-naive and offset-aware datetimes
What this code does
- The naive value has no zone, the aware one has UTC.
- Python refuses to compare them rather than guess.
Comparing naive < aware raises TypeError. That is a feature: it stops a silent mix-up.
ZoneInfo and astimezone
ZoneInfo("Asia/Kolkata") gives a zone by its standard name. astimezone(zone) shows the same moment on another zone's clock. The moment does not change; only the clock reading does.
from datetime import datetimefrom zoneinfo import ZoneInfo
utc = datetime(2026, 11, 8, 9, 0, tzinfo=ZoneInfo("UTC"))print(utc.astimezone(ZoneInfo("Asia/Kolkata")))Example 2 · One moment, three clocks
from datetime import datetimefrom zoneinfo import ZoneInfo
utc = datetime(2026, 11, 8, 9, 0, tzinfo=ZoneInfo("UTC"))for name in ["Asia/Kolkata", "Europe/London", "America/New_York"]: local = utc.astimezone(ZoneInfo(name)) print(name, local.strftime("%H:%M %z"))print(utc == utc.astimezone(ZoneInfo("Asia/Kolkata")))Asia/Kolkata 14:30 +0530 Europe/London 09:00 +0000 America/New_York 04:00 -0500 True
What this code does
%zshows the offset from UTC: +0530 for Kolkata, -0500 for New York in November.- The two datetimes are equal because they are the same moment.
On Windows, run pip install tzdata once so Python has the zone database.
Store UTC, show local
UTC is the world reference time with no daylight saving and a fixed offset of zero. Systems store timestamps in UTC and convert to a local zone for display.
Save timestamps in UTC, which never shifts for daylight saving. Convert to the viewer's zone only when you display them. Use replace(tzinfo=...) only to label a naive value whose zone you already know.
from datetime import datetimefrom zoneinfo import ZoneInfo
local = datetime(2026, 11, 8, 18, 0, tzinfo=ZoneInfo("Europe/London"))print(local.astimezone(ZoneInfo("UTC")).isoformat())Example 3 · Label, then convert
from datetime import datetimefrom zoneinfo import ZoneInfo
naive = datetime(2026, 11, 8, 9, 0)labelled = naive.replace(tzinfo=ZoneInfo("Asia/Kolkata"))print(labelled.hour)print(labelled.astimezone(ZoneInfo("UTC")).isoformat())9 2026-11-08T03:30:00+00:00
What this code does
replacekept 9 on the clock and only added the zone.- Converting to UTC shows the real moment, 03:30 UTC.
replace relabels the clock reading. astimezone converts the moment. They are not the same.
Daylight saving traps
In some zones clocks jump an hour twice a year. Adding a timedelta to an aware datetime moves the clock reading, not the real elapsed time. For exact durations, convert to UTC first, add, then convert back.
from datetime import datetime, timedeltafrom zoneinfo import ZoneInfo
ny = ZoneInfo("America/New_York")start = datetime(2026, 3, 7, 12, 0, tzinfo=ny)print(start + timedelta(days=1))Example 4 · Add a day across daylight saving
from datetime import datetime, timedeltafrom zoneinfo import ZoneInfo
ny = ZoneInfo("America/New_York")start = datetime(2026, 3, 7, 12, 0, tzinfo=ny)print(start + timedelta(days=1))real = start.astimezone(ZoneInfo("UTC")) + timedelta(hours=24)print(real.astimezone(ny))2026-03-08 12:00:00-04:00 2026-03-08 13:00:00-04:00
What this code does
- The clocks in New York moved forward on 8 March 2026.
- Adding 1 day keeps the clock at 12:00. Adding 24 real hours through UTC gives 13:00 local time.
This keeps 12:00 on the clock, even though only 23 real hours passed.
Next step
Check your understanding
11 quick questions