Time zones with zoneinfo¶
A datetime without a time zone is naive. A datetime with one is aware. Python treats the two as different enough that it refuses to compare or arithmetic them together.
This notebook introduces zoneinfo — the standard-library module (Python 3.9+) for time zone handling — and shows how to create, convert, and compare time-zone-aware datetimes correctly.
Naive versus aware¶
A naive datetime doesn't know what time zone it represents. datetime(2026, 4, 21, 14, 30) might be 14:30 in London, or in Tokyo, or anywhere — the object carries no information.
from datetime import datetime
naive = datetime(2026, 4, 21, 14, 30)
print(naive)
print(naive.tzinfo) # None — it's naive
An aware datetime has a tzinfo attribute telling you which zone it's in. Use zoneinfo.ZoneInfo to build one from an IANA time zone name (the Continent/City form).
from zoneinfo import ZoneInfo
london = ZoneInfo("Europe/London")
aware = datetime(2026, 4, 21, 14, 30, tzinfo=london)
print(aware)
print(aware.tzinfo)
The IANA database covers every inhabited region on Earth. A few names worth knowing: UTC, Europe/London, America/New_York, Asia/Tokyo, Australia/Sydney. Never use abbreviations like "BST" or "EST" — they're ambiguous (EST means something different in the US and Australia). ZoneInfo("UTC") is correct; ZoneInfo("GMT") is also valid but you'll see UTC far more often.
Converting between time zones¶
.astimezone(target) converts an aware datetime to another time zone. The absolute moment doesn't change — only the presentation does.
utc = ZoneInfo("UTC")
tokyo = ZoneInfo("Asia/Tokyo")
new_york = ZoneInfo("America/New_York")
moment = datetime(2026, 4, 21, 14, 30, tzinfo=utc)
print(moment.astimezone(london))
print(moment.astimezone(tokyo))
print(moment.astimezone(new_york))
All three printouts represent the same instant in time — just expressed in different local clocks. Subtracting any of them from each other gives timedelta(0).
a = moment.astimezone(london)
b = moment.astimezone(tokyo)
print(a - b)
The naive/aware error¶
You can't compare, subtract, or order a naive datetime against an aware one. Python refuses outright, because the answer depends on a time zone it wasn't told.
try:
aware - naive
except TypeError as e:
print(f"{type(e).__name__}: {e}")
This is a frequent source of bugs in codebases that mix the two. The usual fix is to make everything aware — see the UTC everywhere concept essay. Attach a time zone on the way in and you never have to worry about it again.
Getting the current aware datetime¶
datetime.now() with no argument is naive — avoid it. datetime.now(tz=...) is aware. datetime.utcnow() exists too, but returns a naive datetime set to UTC — arguably the worst of both worlds. Don't use it.
# Good: aware datetime in UTC
now_utc = datetime.now(tz=utc)
print(now_utc)
print(now_utc.tzinfo)
# Also fine: aware datetime in a specific zone
now_london = datetime.now(tz=london)
print(now_london)
Daylight saving transitions¶
zoneinfo handles DST automatically, using the IANA database. Two moments to watch for:
- The spring forward gap — an hour that doesn't exist. In London, 01:30 on the day the clocks go forward isn't a real moment.
- The autumn fall-back overlap — an hour that happens twice. 01:30 on the day the clocks go back happens once before and once after the transition.
zoneinfo picks a reasonable default for each (the interpretation before the transition), but if you care, you can be explicit via the fold attribute. Most application code doesn't need to:
# Spring forward in London: 2026-03-29 01:00 skips to 02:00.
# Just before: 00:30 BST→UTC... actually, just before is GMT. Let's
# just check what zoneinfo says about these two moments around the transition.
before = datetime(2026, 3, 29, 0, 30, tzinfo=london)
after = datetime(2026, 3, 29, 2, 30, tzinfo=london)
print("UTC offsets:", before.utcoffset(), after.utcoffset())
print("Actual gap:", after - before) # 2 hours clock, but only 1 real
The UTC offset flips from 0:00:00 to 1:00:00 across the transition — that's the DST change. The "2 hours" reported is the elapsed clock time, which correctly represents 1 hour of real elapsed time because we skipped an hour on the clock.
In the autumn, the inverse happens — same logic, other direction. See the avoid common date/time mistakes recipe for the specific traps.
Serialising aware datetimes¶
ISO 8601 with a UTC offset is the sane choice:
moment = datetime(2026, 4, 21, 14, 30, tzinfo=london)
print(moment.isoformat()) # includes the offset
# Round-trip
parsed = datetime.fromisoformat(moment.isoformat())
print(parsed)
print(parsed.tzinfo)
The round-tripped value has a timezone fixed-offset, not a ZoneInfo — enough to represent the same instant, but stripped of the zone's name and DST rules. If you need the zone name, store it separately or re-attach ZoneInfo after parsing.
Exercise¶
A meeting is scheduled for 2026-09-15 15:00 London time. Write code that:
- Constructs the aware datetime for the meeting.
- Prints the equivalent local time for New York and Tokyo.
- Calculates how many hours until the meeting, given it's currently
2026-09-14 22:00UTC.
Use ZoneInfo for the zones.
# Your code here
Solution
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
london = ZoneInfo("Europe/London")
new_york = ZoneInfo("America/New_York")
tokyo = ZoneInfo("Asia/Tokyo")
meeting = datetime(2026, 9, 15, 15, 0, tzinfo=london)
print(f"New York: {meeting.astimezone(new_york)}")
print(f"Tokyo: {meeting.astimezone(tokyo)}")
now = datetime(2026, 9, 14, 22, 0, tzinfo=timezone.utc)
gap = meeting - now
print(f"Hours until meeting: {gap.total_seconds() / 3600}")
Recap¶
- Naive datetimes carry no time zone; aware ones do via
tzinfo. - Use
zoneinfo.ZoneInfo("Region/City")to buildtzinfoobjects from the IANA database. .astimezone(target)converts between zones. The instant doesn't change, the presentation does.- Avoid
datetime.now()(naive) anddatetime.utcnow()(naive but UTC). Usedatetime.now(tz=...). - Python refuses to compare or subtract naive against aware — a feature, not a bug.
- DST is handled by
zoneinfovia the IANA database; the edge cases are in the avoid common mistakes recipe.
That's the core of datetime. The recipes and reference pages go deeper into specific tasks.