When you work with dates and times in Python, you eventually need to handle Python time zones. A meeting scheduled for 10:00 AM in New York happens at a different UTC moment than a meeting at 10:00 AM in Tokyo. Python's zoneinfo module, available since Python 3.9, provides access to the IANA time zone database so you can create timezone-aware datetimes and convert between zones.
from datetime import datetime
from zoneinfo import ZoneInfo
ny_time = datetime(2026, 7, 12, 10, 0, tzinfo=ZoneInfo("America/New_York"))
tokyo_time = datetime(2026, 7, 12, 10, 0, tzinfo=ZoneInfo("Asia/Tokyo"))
print(ny_time) # 2026-07-12 10:00:00-04:00
print(tokyo_time) # 2026-07-12 10:00:00+09:00
print(ny_time < tokyo_time) # TrueBoth datetimes say 10:00 AM, but New York is at UTC-4 while Tokyo is at UTC+9. Tokyo's 10:00 AM happens 13 hours earlier in real time, so the comparison correctly shows New York's meeting comes first.
Naive vs aware datetimes
A datetime is naive when it has no time zone information. It is aware when it carries a tzinfo attribute.
from datetime import datetime, timezone
naive = datetime(2026, 7, 12, 10, 0)
aware = datetime(2026, 7, 12, 10, 0, tzinfo=timezone.utc)
print(naive.tzinfo) # None
print(aware.tzinfo) # UTCA naive datetime has tzinfo=None. You cannot reliably compare a naive datetime with an aware one, and you cannot convert a naive datetime to another time zone without first attaching a zone to it.
| Naive datetime | Aware datetime | |
|---|---|---|
| Has tzinfo | No (None) | Yes |
| Safe to compare across zones | No | Yes |
| Can call astimezone() | No | Yes |
| Typical source | time.strptime, plain datetime() | datetime.now(timezone.utc), ZoneInfo attached |
Python raises an error if you try to subtract an aware datetime from a naive one:
from datetime import datetime, timezone
naive = datetime(2026, 7, 12, 10, 0)
aware = datetime(2026, 7, 12, 10, 0, tzinfo=timezone.utc)
print(aware - naive)Running this code raises an error, because Python refuses to guess how a naive datetime relates to a zone. The traceback tells you exactly what went wrong:
TypeError: can't subtract offset-naive and offset-aware datetimesAlways know whether your datetimes are naive or aware before doing arithmetic or comparisons.
Getting the current time in UTC
Use datetime.now(timezone.utc) to get the current UTC time as an aware datetime.
from datetime import datetime, timezone
now_utc = datetime.now(timezone.utc)
print(now_utc) # 2026-07-12 18:30:05.123456+00:00
print(now_utc.tzinfo) # UTCThe +00:00 in the output confirms this is a UTC datetime. Always prefer this over datetime.utcnow(), which returns a naive datetime and has been deprecated in newer Python versions.
Working with IANA time zones
The zoneinfo module reads time zone data from your system's IANA database. You specify a time zone with its IANA key, like "America/New_York" or "Asia/Tokyo".
from datetime import datetime
from zoneinfo import ZoneInfo
meeting = datetime(2026, 11, 15, 9, 0, tzinfo=ZoneInfo("America/Chicago"))
print(meeting) # 2026-11-15 09:00:00-06:00In November, Chicago is at UTC-6 (Central Standard Time). The offset is automatically correct, accounting for whether daylight saving time is in effect on that date.
If the same code ran in July, ZoneInfo would report UTC-5 instead, because Chicago observes Central Daylight Time in summer. You never have to track these transitions yourself, since the IANA database that zoneinfo reads from is updated whenever a country changes its daylight saving rules.
You can list available time zones with zoneinfo.available_timezones(), though the list is large. A few commonly used keys:
| IANA key | Region |
|---|---|
| "UTC" | Coordinated Universal Time |
| "America/New_York" | US Eastern |
| "America/Chicago" | US Central |
| "America/Denver" | US Mountain |
| "America/Los_Angeles" | US Pacific |
| "Europe/London" | United Kingdom |
| "Europe/Berlin" | Central European |
| "Asia/Tokyo" | Japan |
| "Asia/Kolkata" | India |
| "Australia/Sydney" | Australian Eastern |
Avoid three-letter abbreviations like "EST" or "PST". They are ambiguous and do not handle daylight saving transitions correctly.
Converting between time zones
Use the astimezone() method to convert an aware datetime from one time zone to another.
from datetime import datetime
from zoneinfo import ZoneInfo
utc_time = datetime(2026, 7, 12, 15, 0, tzinfo=ZoneInfo("UTC"))
ny_time = utc_time.astimezone(ZoneInfo("America/New_York"))
tokyo_time = utc_time.astimezone(ZoneInfo("Asia/Tokyo"))
print(ny_time) # 2026-07-12 11:00:00-04:00
print(tokyo_time) # 2026-07-13 00:00:00+09:003:00 PM UTC is 11:00 AM in New York and midnight the next day in Tokyo. astimezone handles the math and adjusts the date when the conversion crosses midnight.
Attaching a time zone to a naive datetime
When you receive a naive datetime from user input or a database, attach a time zone with replace().
from datetime import datetime
from zoneinfo import ZoneInfo
user_input = datetime(2026, 7, 12, 14, 30)
aware = user_input.replace(tzinfo=ZoneInfo("America/Chicago"))
print(aware) # 2026-07-12 14:30:00-05:00replace(tzinfo=...) tells Python "this naive datetime should be interpreted as Chicago time." It does not convert the hour value. Use this only when you know the datetime already represents the intended local time.
Making a naive datetime safe with UTC
When you do not know the original time zone of a naive datetime, the safest approach is to assume UTC.
from datetime import datetime, timezone
naive = datetime(2026, 7, 12, 14, 30)
aware_utc = naive.replace(tzinfo=timezone.utc)
print(aware_utc) # 2026-07-12 14:30:00+00:00This treats the original 14:30 as UTC. If the datetime actually came from local time, you need to attach the correct local zone instead and then convert to UTC if needed.
Practical example: displaying event times
A common pattern is to store events in UTC and display them in the viewer's local time.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
event_time_utc = datetime(2026, 12, 25, 0, 0, tzinfo=timezone.utc)
for zone_name in ["America/New_York", "Europe/London", "Asia/Kolkata"]:
local = event_time_utc.astimezone(ZoneInfo(zone_name))
print(f"{zone_name}: {local.strftime('%Y-%m-%d %H:%M %Z')}")The loop converts the same UTC moment into three different local times, one for each viewer, and prints them side by side so you can compare the results directly:
America/New_York: 2026-12-24 19:00 EST
Europe/London: 2026-12-25 00:00 GMT
Asia/Kolkata: 2026-12-25 05:30 ISTMidnight UTC on Christmas arrives at 7:00 PM on December 24 in New York, midnight in London, and 5:30 AM on December 25 in India. Each user sees the correct local time for the same UTC moment.
For more on formatting datetimes, see Format Dates and Times in Python.
Common mistakes
Mixing naive and aware datetimes. Comparing or subtracting a naive datetime from an aware one raises a TypeError. Always make both datetimes aware before operating on them.
Using pytz instead of zoneinfo. The pytz library was the standard before Python 3.9, but it has confusing behavior with localize() vs astimezone(). For new code, use the built-in zoneinfo module. It is simpler and follows the standard library conventions.
Using fixed offsets instead of IANA zones. A fixed offset like timezone(timedelta(hours=-5)) does not handle daylight saving time. A place that is normally UTC-5 might be UTC-4 in summer. Use ZoneInfo("America/Chicago") instead of a hardcoded offset.
Forgetting that datetime.now() returns a naive datetime. datetime.now() gives local time but without a tzinfo. This datetime cannot be safely converted to other zones. Always pass a time zone: datetime.now(timezone.utc) or datetime.now(ZoneInfo("America/New_York")).
Rune AI
Key Insights
- Use
datetime.timezone.utcfor UTC andZoneInfo(\"zone name\")for IANA time zones. - A timezone-aware datetime carries zone information; a naive datetime does not.
- Store timestamps in UTC and convert to local time zones only when displaying to users.
- Use
.astimezone()to convert a datetime from one time zone to another. - Prefer the built-in
zoneinfomodule over the third-partypytzlibrary for new code. - Always attach a time zone when creating datetimes from user input.
Frequently Asked Questions
What does 'timezone-aware' mean in Python?
Should I store dates in UTC or local time?
How do I get the user's local time zone?
Conclusion
Time zones are a common source of bugs, but Python's zoneinfo module (available since Python 3.9) makes them manageable. Always store UTC, attach time zones to datetimes with ZoneInfo, and convert to local time only at display time. Avoid pytz for new code unless you are maintaining a legacy project.
More in this topic
Format Dates and Times in Python
Learn how to use strftime and strptime to format dates as strings and parse date strings into Python datetime objects.
What Is the Python Standard Library?
Learn what the Python standard library is, why it matters, and how to use its built-in modules without installing anything extra.
Encode and Decode Base64 Data in Python
Learn how to use Python's base64 module to encode binary data as text and decode Base64 strings back to bytes.