GeezerVisonThe World Wide Web, restored to 800 pixels of glory. |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites |
GeezerVison is showing zetcode.com — Python zoneinfo - working with time zones in Python Python zoneinfolast modified October 29, 2026 Python 3.15
In this article we show how to work with time zones in Python using the
The
The IANA time zone database (also called tz or zoneinfo database)
records the history of UTC offsets and daylight saving time rules for
locations around the world. Zones are identified by keys such as
The basics of dates and times are covered in the Python datetime article. A first time zone
A first_zone.py
from datetime import datetime
from zoneinfo import ZoneInfo
tz = ZoneInfo('Europe/Bratislava')
dt = datetime(2026, 7, 15, 14, 30, tzinfo=tz)
print(dt)
print(dt.tzname())
print(dt.utcoffset())
print(dt.dst())
print(tz.key)
We create an aware datetime, one that knows its time zone, for 2:30 PM in Bratislava on a summer day. dt = datetime(2026, 7, 15, 14, 30, tzinfo=tz) The zone does not store a single offset. When we ask for it, the zone looks up the rules in force on that date: in July, Central Europe uses summer time. print(dt.tzname()) print(dt.utcoffset()) print(dt.dst())
The $ python first_zone.py 2026-07-15 14:30:00+02:00 CEST 2:00:00 1:00:00 Europe/Bratislava World clock
The world_clock.py
from datetime import datetime, UTC
from zoneinfo import ZoneInfo
cities = {
'Bratislava': 'Europe/Bratislava',
'London': 'Europe/London',
'New York': 'America/New_York',
'Tokyo': 'Asia/Tokyo',
'Kolkata': 'Asia/Kolkata',
'Kathmandu': 'Asia/Kathmandu',
'Sydney': 'Australia/Sydney',
}
now = datetime.now(UTC)
print(f'UTC: {now:%Y-%m-%d %H:%M}')
for city, key in cities.items():
local = now.astimezone(ZoneInfo(key))
print(f'{city:<12} {local:%Y-%m-%d %H:%M} {local.tzname():<6} {local:%z}')
We take the current time in UTC and show it in several cities. Kolkata and Kathmandu show that offsets are not always whole hours. now = datetime.now(UTC)
Passing a time zone to print(f'{city:<12} {local:%Y-%m-%d %H:%M} {local.tzname():<6} {local:%z}')
The $ python world_clock.py UTC: 2026-10-29 21:41 Bratislava 2026-10-29 22:41 CET +0100 London 2026-10-29 21:41 GMT +0000 New York 2026-10-29 17:41 EDT -0400 Tokyo 2026-10-30 06:41 JST +0900 Kolkata 2026-10-30 03:11 IST +0530 Kathmandu 2026-10-30 03:26 +0545 +0545 Sydney 2026-10-30 08:41 AEDT +1100 Scheduling a meetingA typical task is to tell participants in other countries when a meeting takes place. meeting.py
from datetime import datetime
from zoneinfo import ZoneInfo
meeting = datetime(2026, 11, 12, 16, 0, tzinfo=ZoneInfo('Europe/Berlin'))
for key in ['America/Los_Angeles', 'America/Sao_Paulo', 'Asia/Singapore',
'Pacific/Auckland']:
local = meeting.astimezone(ZoneInfo(key))
print(f'{key:<20} {local:%a %d %b %H:%M}')
The meeting is scheduled for 4 PM Berlin time. We convert it to the local times of four participants. In Auckland, the meeting falls on the next day. $ python meeting.py America/Los_Angeles Thu 12 Nov 07:00 America/Sao_Paulo Thu 12 Nov 12:00 Asia/Singapore Thu 12 Nov 23:00 Pacific/Auckland Fri 13 Nov 04:00 Naive and aware datetimes
A datetime without naive_aware.py
from datetime import datetime
from zoneinfo import ZoneInfo
naive = datetime(2026, 7, 15, 9, 0)
print(naive, naive.tzinfo)
attached = naive.replace(tzinfo=ZoneInfo('America/Chicago'))
print(attached)
converted = attached.astimezone(ZoneInfo('Asia/Tokyo'))
print(converted)
print(attached == converted)
try:
print(naive < attached)
except TypeError as e:
print('TypeError:', e)
We attach the Chicago zone to a naive 9 AM and convert the result to Tokyo time. attached = naive.replace(tzinfo=ZoneInfo('America/Chicago'))
Using print(attached == converted)
Aware datetimes are compared as instants, so 9 AM in Chicago equals 11 PM in
Tokyo. Naive and aware datetimes cannot be ordered; Python raises a
$ python naive_aware.py 2026-07-15 09:00:00 None 2026-07-15 09:00:00-05:00 2026-07-15 23:00:00+09:00 True TypeError: can't compare offset-naive and offset-aware datetimes Daylight saving timeThe offset of a zone changes during the year when it observes daylight saving time. dst_offsets.py
from datetime import datetime
from zoneinfo import ZoneInfo
tz = ZoneInfo('Europe/Bratislava')
for month in (1, 3, 4, 7, 10, 11):
dt = datetime(2026, month, 1, 12, 0, tzinfo=tz)
print(f'{dt:%b} {dt.tzname()} offset {dt.utcoffset()} dst {dt.dst()}')
We check the offset of the Bratislava zone at noon on the first day of several months. Summer time (CEST) is in force from the end of March to the end of October. $ python dst_offsets.py Jan CET offset 1:00:00 dst 0:00:00 Mar CET offset 1:00:00 dst 0:00:00 Apr CEST offset 2:00:00 dst 1:00:00 Jul CEST offset 2:00:00 dst 1:00:00 Oct CEST offset 2:00:00 dst 1:00:00 Nov CET offset 1:00:00 dst 0:00:00 Finding transitions
The transitions.py
from datetime import datetime, timedelta, UTC
from zoneinfo import ZoneInfo
def transitions(key, year):
tz = ZoneInfo(key)
t = datetime(year, 1, 1, tzinfo=UTC)
end = datetime(year + 1, 1, 1, tzinfo=UTC)
prev = t.astimezone(tz).utcoffset()
while t < end:
t += timedelta(hours=1)
offset = t.astimezone(tz).utcoffset()
if offset != prev:
before = (t - timedelta(hours=1)).astimezone(tz)
after = t.astimezone(tz)
print(f'{key}: {before:%Y-%m-%d %H:%M %Z} -> {after:%H:%M %Z}')
prev = offset
for key in ['Europe/Bratislava', 'America/New_York', 'Australia/Sydney',
'Asia/Tokyo']:
transitions(key, 2026)
We step through the year hour by hour. Stepping in UTC, not in local time, guarantees that each step is exactly one hour long. before = (t - timedelta(hours=1)).astimezone(tz) after = t.astimezone(tz) When the offset changes, we print the local time just before and just after the change. In spring, clocks jump from 2:00 to 3:00, and in autumn they fall back from 3:00 to 2:00. Sydney is in the southern hemisphere, so its summer time starts in October. Tokyo does not observe daylight saving time and prints nothing. $ python transitions.py Europe/Bratislava: 2026-03-29 01:00 CET -> 03:00 CEST Europe/Bratislava: 2026-10-25 02:00 CEST -> 02:00 CET America/New_York: 2026-03-08 01:00 EST -> 03:00 EDT America/New_York: 2026-11-01 01:00 EDT -> 01:00 EST Australia/Sydney: 2026-04-05 02:00 AEDT -> 02:00 AEST Australia/Sydney: 2026-10-04 01:00 AEST -> 03:00 AEDT Ambiguous times and fold
When clocks fall back, an hour of local time repeats. In Bratislava,
2:30 AM on 25 October 2026 happens twice: first in CEST, then in CET. The
fold_ambiguous.py
from datetime import datetime, UTC
from zoneinfo import ZoneInfo
tz = ZoneInfo('Europe/Bratislava')
first = datetime(2026, 10, 25, 2, 30, tzinfo=tz)
second = first.replace(fold=1)
for dt in (first, second):
print(dt, dt.tzname(), dt.fold, dt.astimezone(UTC))
print(first == second)
print(second - first)
print(second.astimezone(UTC) - first.astimezone(UTC))
The default print(first == second) print(second - first)
These results are surprising. Datetimes that share the same
$ python fold_ambiguous.py 2026-10-25 02:30:00+02:00 CEST 0 2026-10-25 00:30:00+00:00 2026-10-25 02:30:00+01:00 CET 1 2026-10-25 01:30:00+00:00 True 0:00:00 1:00:00 Missing timesWhen clocks spring forward, an hour of local time is skipped. In Bratislava, there is no 2:30 AM on 29 March 2026. Python does not raise an error for such times. gap.py
from datetime import datetime, UTC
from zoneinfo import ZoneInfo
tz = ZoneInfo('Europe/Bratislava')
def exists(dt):
roundtrip = dt.astimezone(UTC).astimezone(dt.tzinfo)
return roundtrip.replace(tzinfo=None) == dt.replace(tzinfo=None)
for hour, minute in [(1, 30), (2, 30), (3, 30)]:
dt = datetime(2026, 3, 29, hour, minute, tzinfo=tz)
print(dt, exists(dt), dt.astimezone(UTC).astimezone(tz))
A missing time does not survive a round trip through UTC: 2:30 AM is interpreted with the offset in force before the transition and comes back as 3:30 AM CEST. We use this to detect it. def exists(dt):
roundtrip = dt.astimezone(UTC).astimezone(dt.tzinfo)
return roundtrip.replace(tzinfo=None) == dt.replace(tzinfo=None)
The function compares the wall times before and after the round trip. Applications that accept local times from users, such as calendars, should check for missing times and ask the user or move the time forward. $ python gap.py 2026-03-29 01:30:00+01:00 True 2026-03-29 01:30:00+01:00 2026-03-29 02:30:00+01:00 False 2026-03-29 03:30:00+02:00 2026-03-29 03:30:00+02:00 True 2026-03-29 03:30:00+02:00 Arithmetic across transitions
Adding a arithmetic.py
from datetime import datetime, timedelta, UTC
from zoneinfo import ZoneInfo
tz = ZoneInfo('America/New_York')
start = datetime(2026, 11, 1, 0, 0, tzinfo=tz)
later = start + timedelta(hours=24)
print(start)
print(later)
print(later - start)
real = later.astimezone(UTC) - start.astimezone(UTC)
print(real)
exact = (start.astimezone(UTC) + timedelta(hours=24)).astimezone(tz)
print(exact)
On 1 November 2026, New York switches from EDT to EST, so that day has 25 hours. later = start + timedelta(hours=24) Adding 24 hours to midnight gives midnight of the next day, which is 25 real hours later. Subtracting the two values in the same zone gives one day, while subtracting them in UTC gives the real elapsed time. exact = (start.astimezone(UTC) + timedelta(hours=24)).astimezone(tz) To add an exact duration, we do the arithmetic in UTC and convert back. Use wall-time arithmetic for things like "the same time tomorrow" and UTC arithmetic for elapsed time, such as timeouts. $ python arithmetic.py 2026-11-01 00:00:00-04:00 2026-11-02 00:00:00-05:00 1 day, 0:00:00 1 day, 1:00:00 2026-11-01 23:00:00-05:00 Timestamps and ISO stringsUnix timestamps and ISO 8601 strings are common ways to exchange times. timestamps.py
from datetime import datetime
from zoneinfo import ZoneInfo
ts = 1_800_000_000
for key in ['UTC', 'Europe/Bratislava', 'America/Denver']:
dt = datetime.fromtimestamp(ts, tz=ZoneInfo(key))
print(f'{key:<18} {dt.isoformat()}')
parsed = datetime.fromisoformat('2027-01-15T08:00:00+01:00')
print(parsed.tzinfo)
print(parsed.astimezone(ZoneInfo('Asia/Tokyo')))
The parsed = datetime.fromisoformat('2027-01-15T08:00:00+01:00')
An ISO string carries only an offset, not a zone, so the parsed datetime has
a fixed-offset $ python timestamps.py UTC 2027-01-15T08:00:00+00:00 Europe/Bratislava 2027-01-15T09:00:00+01:00 America/Denver 2027-01-15T01:00:00-07:00 UTC+01:00 2027-01-15 16:00:00+09:00 UTC and fixed offsets
The utc_zones.py
from datetime import datetime, timedelta, timezone, UTC
from zoneinfo import ZoneInfo
dt = datetime(2026, 7, 15, 12, 0)
zones = [UTC, timezone(timedelta(hours=2)), ZoneInfo('UTC'),
ZoneInfo('Europe/Bratislava')]
for tz in zones:
aware = dt.replace(tzinfo=tz)
print(f'{type(tz).__name__:<9} {aware} {aware.tzname()}')
january = datetime(2026, 1, 15, 12, 0)
print(january.replace(tzinfo=zones[1]).tzname(),
january.replace(tzinfo=zones[3]).tzname())
For UTC, $ python utc_zones.py timezone 2026-07-15 12:00:00+00:00 UTC timezone 2026-07-15 12:00:00+02:00 UTC+02:00 ZoneInfo 2026-07-15 12:00:00+00:00 UTC ZoneInfo 2026-07-15 12:00:00+02:00 CEST UTC+02:00 CET Available time zones
The list_zones.py
from collections import Counter
from zoneinfo import available_timezones
zones = available_timezones()
print(len(zones))
regions = Counter(key.split('/')[0] for key in zones if '/' in key)
for region, count in regions.most_common(6):
print(f'{region:<12} {count}')
slovak = sorted(key for key in zones if 'Brat' in key or 'Prague' in key)
print(slovak)
We count the zones, group them by region, and look for two keys. The function scans the whole database, so it is slow; call it once and keep the result. The numbers depend on the version of the database installed on the system. $ python list_zones.py 598 America 169 Asia 99 Europe 64 Africa 54 Pacific 44 Etc 35 ['Europe/Bratislava', 'Europe/Prague'] Invalid keys
The not_found.py
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
for key in ['Europe/Kosice', 'europe/bratislava', '../etc/passwd', 'Europe/Prague']:
try:
tz = ZoneInfo(key)
print(f'{key}: {tz}')
except ZoneInfoNotFoundError as e:
print(f'{key}: not found: {e}')
except ValueError as e:
print(f'{key}: invalid key: {e}')
Košice is in Slovakia, but the database names zones after one city per
region, so there is no $ python not_found.py Europe/Kosice: not found: 'No time zone found with key Europe/Kosice' europe/bratislava: not found: 'No time zone found with key europe/bratislava' ../etc/passwd: invalid key: ZoneInfo keys must refer to subdirectories of TZPATH, got: ../etc/passwd Europe/Prague: Europe/Prague The cache
zone_cache.py
from zoneinfo import ZoneInfo
a = ZoneInfo('Asia/Tokyo')
b = ZoneInfo('Asia/Tokyo')
c = ZoneInfo.no_cache('Asia/Tokyo')
print(a is b)
print(a is c)
print(a == c)
ZoneInfo.clear_cache()
d = ZoneInfo('Asia/Tokyo')
print(a is d)
The cache makes zone creation cheap and is required for correct comparisons:
datetimes in the same zone are compared by wall time only if they share the
same c = ZoneInfo.no_cache('Asia/Tokyo')
The $ python zone_cache.py True False False False The time zone database
The zone_source.py
import zoneinfo
from pathlib import Path
print(zoneinfo.TZPATH)
path = next(Path(p) / 'Europe/Vienna' for p in zoneinfo.TZPATH
if (Path(p) / 'Europe/Vienna').exists())
print(path)
with path.open('rb') as f:
print(f.read(4))
f.seek(0)
tz = zoneinfo.ZoneInfo.from_file(f, key='Vienna (from file)')
print(tz)
print(tz == zoneinfo.ZoneInfo('Europe/Vienna'))
We find the file for Vienna and load it with tz = zoneinfo.ZoneInfo.from_file(f, key='Vienna (from file)')
A zone created from a file is not cached and its key is only a label. If no
system database is found, Python falls back to the $ python zone_source.py
('/usr/share/zoneinfo', '/usr/lib/zoneinfo', '/usr/share/lib/zoneinfo', '/etc/zoneinfo')
/usr/share/zoneinfo/Europe/Vienna
b'TZif'
Vienna (from file)
False
Pickling
Datetimes with pickling.py
import pickle
from datetime import datetime
from zoneinfo import ZoneInfo
dt = datetime(2026, 12, 24, 18, 0, tzinfo=ZoneInfo('Europe/Bratislava'))
data = pickle.dumps(dt)
print(len(data))
print(b'Europe/Bratislava' in data)
restored = pickle.loads(data)
print(restored)
print(restored.tzinfo is dt.tzinfo)
A zone is pickled by its key, not by its data, so the pickle is small. When loaded, the zone is created again from the key and the cache returns the same object. $ python pickling.py 142 True 2026-12-24 18:00:00+01:00 True Sourcezoneinfo – IANA time zone support PEP 615 – Support for the IANA Time Zone Database in the Standard Library
In this article we have worked with time zones using the Python
AuthorMy name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming. List all Python tutorials. |
| You are visitor 01402520 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |