Time Zones with zoneinfo
Reviewed & published by Brayan K
zoneinfo is the standard-library module that attaches real IANA time zones — like America/New_York — to a datetime, turning an ambiguous wall-clock reading into an exact, daylight-saving-aware moment in time.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Dates and times look simple until two cities, a daylight-saving change, and a midnight deadline collide. Once you understand aware datetimes and store everything in UTC, time-zone bugs simply stop happening.
What You'll Learn in This Lesson
- The crucial difference between naive and aware datetimes
- Creating zones with zoneinfo.ZoneInfo
- Attaching a time zone to a datetime
- Converting between zones with astimezone
- Storing in UTC as a best practice
- How DST changes a zone's offset automatically
1 Naive vs Aware Datetimes
A naive datetime has no tzinfo — it's "12:00 on Jan 15" with no clue which 12:00. An aware datetime carries a zone, pinning it to one exact instant on the globe:
from datetime import datetime
from zoneinfo import ZoneInfo
naive = datetime(2024, 1, 15, 12, 0, 0)
print("Naive: ", naive) # 2024-01-15 12:00:00
print("tzinfo:", naive.tzinfo) # None <- naive!
aware = datetime(2024, 1, 15, 12, 0, 0,
tzinfo=ZoneInfo("America/New_York"))
print("Aware: ", aware) # 2024-01-15 12:00:00-05:00
print("tzinfo:", aware.tzinfo) # America/New_Yorknaive -> 2024-01-15 12:00:00 (no offset shown)
aware -> 2024-01-15 12:00:00-05:00 (the -05:00 = its zone)That trailing -05:00 is the zone's offset from UTC. Its presence is how you know a datetime is aware and can be safely compared or converted.
🌐 Creating Zones & Attaching Them
ZoneInfo("Area/City") builds a real IANA time zone. Pass it as tzinfo when constructing a datetime to make that wall-clock reading aware:
from datetime import datetime
from zoneinfo import ZoneInfo
# A meeting at 9am, local New York time
meeting = datetime(2024, 6, 1, 9, 0, 0,
tzinfo=ZoneInfo("America/New_York"))
print(meeting.isoformat()) # 2024-06-01T09:00:00-04:00
print(meeting.tzname()) # EDT (Eastern Daylight Time)
# A few zone names you'll use a lot:
for name in ["UTC", "Europe/London", "Asia/Tokyo", "Australia/Sydney"]:
print(name, "->", ZoneInfo(name))Note 9:00 in June shows -04:00 (EDT), because New York is on daylight saving time then. zoneinfo applies the correct offset for the date automatically.
2 Converting Between Zones with astimezone
.astimezone(other_zone) returns the same instant expressed in a different zone — the clock reading changes, but the actual moment does not:
from datetime import datetime
from zoneinfo import ZoneInfo
ny = datetime(2024, 6, 1, 9, 0, 0,
tzinfo=ZoneInfo("America/New_York"))
london = ny.astimezone(ZoneInfo("Europe/London"))
tokyo = ny.astimezone(ZoneInfo("Asia/Tokyo"))
print("New York:", ny.isoformat()) # 2024-06-01T09:00:00-04:00
print("London: ", london.isoformat()) # 2024-06-01T14:00:00+01:00
print("Tokyo: ", tokyo.isoformat()) # 2024-06-01T22:00:00+09:00from datetime import datetime
from zoneinfo import ZoneInfo
# A product launch fixed at 10:00 in San Francisco
launch = datetime(2024, 9, 10, 10, 0, 0,
tzinfo=ZoneInfo("America/Los_Angeles"))
print("San Francisco:", launch.isoformat())
print("New York: ", launch.astimezone(ZoneInfo("America/New_York")).isoformat())
print("London: ", launch.astimezone(ZoneInfo("Europe/London")).isoformat())
print("Sydney: ", launch.astimezone(ZoneInfo("Australia/Sydney")).isoformat())
# ✅ Expected output:
# San Francisco: 2024-09-10T10:00:00-07:00
# New York: 2024-09-10T13:00:00-04:00
# London: 2024-09-10T18:00:00+01:00
# Sydney: 2024-09-11T03:00:00+10:003 Store in UTC, Display Locally
The professional pattern: keep all stored and computed times in UTC (which never shifts), and convert to a local zone only at the moment you show it to a user:
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
# Store the canonical moment in UTC
event_utc = datetime(2024, 7, 4, 16, 30, 0, tzinfo=timezone.utc)
print("Stored (UTC):", event_utc.isoformat()) # ...T16:30:00+00:00
# Display it in whatever zone the viewer is in
la = event_utc.astimezone(ZoneInfo("America/Los_Angeles"))
ist = event_utc.astimezone(ZoneInfo("Asia/Kolkata"))
print("Los Angeles:", la.isoformat()) # 2024-07-04T09:30:00-07:00
print("India: ", ist.isoformat()) # 2024-07-04T22:00:00+05:30☀️ Daylight Saving Time, Handled for You
The same zone can have different offsets depending on the date, because of daylight saving time. zoneinfo knows the rules, so the offset and abbreviation update automatically:
from datetime import datetime
from zoneinfo import ZoneInfo
ny = ZoneInfo("America/New_York")
winter = datetime(2024, 1, 1, 12, 0, tzinfo=ny) # standard time
summer = datetime(2024, 7, 1, 12, 0, tzinfo=ny) # daylight time
print("January noon:", winter.isoformat(), winter.tzname())
print("July noon:", summer.isoformat(), summer.tzname())
# January noon: 2024-01-01T12:00:00-05:00 EST
# July noon: 2024-07-01T12:00:00-04:00 EDT🎯 Your Turn: Fill in the Blank
Complete the code to make a UTC time and convert it to Tokyo. Replace each ___, then run it.
# 🎯 YOUR TURN: replace each ___
from datetime import datetime, timezone
from zoneinfo import ___ # the class that builds a zone
# A deadline stored in UTC
deadline = datetime(2024, 12, 31, 23, 0, 0, tzinfo=___.utc)
print("UTC: ", deadline.isoformat())
# Convert it to Tokyo local time
tokyo = deadline.___(ZoneInfo("Asia/Tokyo")) # method to convert
print("Tokyo:", tokyo.isoformat())
# Expected output:
# UTC: 2024-12-31T23:00:00+00:00
# Tokyo: 2025-01-01T08:00:00+09:00from datetime import datetime, timezone
from zoneinfo import ZoneInfo
deadline = datetime(2024, 12, 31, 23, 0, 0, tzinfo=timezone.utc)
print("UTC: ", deadline.isoformat()) # ...T23:00:00+00:00
tokyo = deadline.astimezone(ZoneInfo("Asia/Tokyo"))
print("Tokyo:", tokyo.isoformat()) # 2025-01-01T08:00:00+09:00Common Errors
❌ Comparing a naive and an aware datetime
a = datetime(2024, 1, 1, 12, 0) # naive
b = datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc) # aware
a < b # TypeError: can't compare offset-naive and offset-aware✅ Make both aware (attach a zone) before comparing or subtracting them.
❌ Swapping tzinfo to "convert"
# WRONG: keeps 9:00 but relabels the zone -> different instant!
london = ny.replace(tzinfo=ZoneInfo("Europe/London"))✅ Use .astimezone(...), which recalculates the clock time for the same moment.
❌ A bad or misspelled zone name
ZoneInfo("America/New York") # space, not underscore
# zoneinfo.ZoneInfoNotFoundError: 'No time zone found...'✅ Use exact IANA names with underscores: America/New_York, Asia/Kolkata, Europe/Paris.
🎯 Mini Challenge: A Meeting Scheduler
A meeting is set for 15:00 UTC. Print its local start time for three offices and show that DST is handled (use a July date).
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
meeting_utc = datetime(2024, 7, 15, 15, 0, 0, tzinfo=timezone.utc)
offices = {
"London": "Europe/London",
"New York": "America/New_York",
"Tokyo": "Asia/Tokyo",
}
print("Meeting (UTC):", meeting_utc.isoformat())
for city, zone in offices.items():
local = meeting_utc.astimezone(ZoneInfo(zone))
print(f"{city:9}: {local.strftime('%H:%M')} ({local.tzname()})")
# ✅ Expected output:
# Meeting (UTC): 2024-07-15T15:00:00+00:00
# London : 16:00 (BST)
# New York : 11:00 (EDT)
# Tokyo : 00:00 (JST)📚 Keep Learning
- zoneinfo — IANA time zone support — the ZoneInfo class, available zones, and data sources.
- datetime — basic date and time types — aware vs naive objects, astimezone, and timezone.utc.
- strftime / strptime formatting — turning datetimes into strings and back for display.
Frequently Asked Questions
Lesson complete — time zones are no longer scary!
You can tell naive from aware datetimes, build real zones with ZoneInfo, attach and convert them with astimezone, store everything in UTC, and let zoneinfo handle daylight saving for you. That's the foundation of correct date-and-time code.
Practice quiz
What distinguishes a naive datetime from an aware one?
- A naive datetime stores the year as a string
- An aware datetime cannot be compared
- A naive datetime has no tzinfo (it's None); an aware one carries a time zone
- There is no difference
Answer: A naive datetime has no tzinfo (it's None); an aware one carries a time zone. A naive datetime's tzinfo is None — just a wall-clock reading; an aware datetime carries a tzinfo, pinning it to an exact instant.
Which standard-library class builds a real IANA time zone?
- ZoneInfo('America/New_York')
- datetime.timezone('America/New_York')
- pytz.timezone('America/New_York')
- TimeZone('America/New_York')
Answer: ZoneInfo('America/New_York'). zoneinfo.ZoneInfo('Area/City') builds a real IANA zone that knows about DST and historical offset changes.
In which Python version was the zoneinfo module added?
- Python 3.6
- Python 3.7
- Python 3.11
- Python 3.9
Answer: Python 3.9. zoneinfo was added to the standard library in Python 3.9, providing IANA time zones without a third-party package.
What does .astimezone(ZoneInfo('Europe/London')) do to an aware datetime?
- Keeps the clock digits and relabels the zone
- Returns the same instant expressed in the new zone (the clock reading changes)
- Raises an error on naive datetimes only
- Deletes the original tzinfo
Answer: Returns the same instant expressed in the new zone (the clock reading changes). astimezone returns the same moment in a different zone — the wall-clock time changes, the actual instant does not.
Why is swapping tzinfo with .replace() to 'convert' a time almost always a bug?
- It keeps the same clock digits but changes the actual instant in time
- It is too slow
- replace() does not exist on datetime
- It always raises ZoneInfoNotFoundError
Answer: It keeps the same clock digits but changes the actual instant in time. replace(tzinfo=...) keeps 9:00 but relabels the zone, changing the real moment; astimezone recalculates the clock for the same instant.
It is 09:00 in New York (EDT) in June. What is the equivalent time in London?
- 09:00
- 13:00
- 14:00
- 22:00
Answer: 14:00. 9am New York in June (-04:00) is 14:00 in London (+01:00) — the same instant shown on two clocks.
Why is storing times in UTC the recommended best practice?
- UTC is always one hour ahead of local time
- UTC has no daylight saving and never shifts, giving an unambiguous baseline
- UTC automatically converts to the user's zone
- Only UTC supports microseconds
Answer: UTC has no daylight saving and never shifts, giving an unambiguous baseline. UTC never observes DST and never changes its rules, so storing/computing in UTC and converting only for display avoids a class of bugs.
How should you get the current time in UTC as an AWARE datetime?
- datetime.utcnow()
- datetime.now()
- ZoneInfo.now()
- datetime.now(timezone.utc)
Answer: datetime.now(timezone.utc). datetime.now(timezone.utc) returns an aware UTC datetime; the deprecated datetime.utcnow() returns a naive one, which is error-prone.
For America/New_York, why does noon in January show -05:00 but noon in July shows -04:00?
- The zone name is misspelled
- Daylight saving time changes the offset; zoneinfo applies the correct one per date
- January has fewer hours
- The datetime is naive in summer
Answer: Daylight saving time changes the offset; zoneinfo applies the correct one per date. zoneinfo knows the DST rules: New York is EST (-05:00) in winter and EDT (-04:00) in summer, applied automatically by date.
What happens with ZoneInfo('America/New York') — a space instead of an underscore?
- It works the same as the underscore form
- It silently defaults to UTC
- It raises ZoneInfoNotFoundError because the IANA name is wrong
- It returns a naive datetime
Answer: It raises ZoneInfoNotFoundError because the IANA name is wrong. IANA names use underscores; 'America/New York' isn't a valid key, so it raises ZoneInfoNotFoundError. Use 'America/New_York'.
Continue this course
- Previous: Custom Exceptions & Exception Chaining
- Next: Serialization with pickle — Save and load Python objects — and the critical security warning
- Quick reference: Python cheat sheet
Frequently asked questions
What's the difference between a naive and an aware datetime?
A naive datetime has no time-zone information attached (its tzinfo is None) — it's just a wall-clock reading with no idea where on Earth it applies. An aware datetime carries a tzinfo, so it represents an exact, unambiguous moment in time. Aware datetimes can be safely compared and converted; naive ones cannot, because you don't know which zone they belong to.
What is zoneinfo and where do its time zones come from?
zoneinfo is a standard-library module (added in Python 3.9) that gives you real IANA time zones like 'America/New_York' or 'Europe/London'. You create one with ZoneInfo('Area/City'). These zones know about daylight saving time and historical offset changes, so attaching one gives you correct, DST-aware behavior automatically.
Why should I store times in UTC?
UTC has no daylight saving time and never shifts, so it's an unambiguous baseline. The best-practice pattern is: store and compute in UTC, then convert to a local zone only when displaying to a user. This avoids a whole class of bugs around DST transitions and zones that change their rules.
How do I convert a datetime from one zone to another?
Use the .astimezone(ZoneInfo('Other/Zone')) method on an aware datetime. It returns a new datetime representing the same instant in the new zone — the clock reading changes but the actual moment does not. Never just swap the tzinfo to convert; that changes the meaning of the time instead of converting it.