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

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_York
naive  ->  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:00
from 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:00

3 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:00
from 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:00

Common 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

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

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.