Python Basics: Time Zones and Clock Changes with zoneinfo
A meeting can fall on different dates in Tokyo and New York. A clock change can even make a local time occur twice. Python’s standard-library zoneinfo module supplies named time zones for these situations.
Before you start
These three independent examples were tested on Linux with Bash, Python 3.12.14 and Python 3.13.5, using the operating system’s IANA time-zone database, version 2026b. The commands use python3; replace it with python3.13 to use that interpreter.
Python does not bundle the time-zone database. ZoneInfo searches system data, then the first-party tzdata package if available. No extra Python package was needed for these runs, and no environment variables need to be set. A custom PYTHONTZPATH can change where Python looks. See the data-source documentation.
Create a fresh working directory in one Bash session:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Save the source blocks here using their stated filenames. Avoid zoneinfo.py and datetime.py, which would shadow standard-library modules. The programs only print results.
First, check that the required time-zone data is available. Save as demo_zoneinfo_check.py:
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
try:
for key in ("Asia/Tokyo", "America/New_York"):
ZoneInfo(key)
except ZoneInfoNotFoundError:
raise SystemExit("Missing time-zone data; stop before running the examples.")
print("Required time zones are available.")Run it before the examples:
python3 demo_zoneinfo_check.pySuccessful output:
Required time zones are available.If it reports missing data, stop. Ask your system administrator to provide the operating system’s time-zone data, or add the first-party tzdata dependency to a project virtual environment. Do not install packages globally just for this walkthrough. The commands here install nothing. Historical examples keep the output independent of today’s date; time-zone database revisions can still affect historical rules.
Show one meeting in two cities
Save as demo_zoneinfo_convert.py:
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
meeting = datetime(2024, 7, 1, 15, 0, tzinfo=timezone.utc)
print("UTC:", meeting.isoformat())
for key in ("Asia/Tokyo", "America/New_York"):
local = meeting.astimezone(ZoneInfo(key))
print(f"{key}: {local.isoformat()}")
print("Round trip:", local.astimezone(timezone.utc) == meeting)Run it:
python3 demo_zoneinfo_convert.pyOutput:
UTC: 2024-07-01T15:00:00+00:00
Asia/Tokyo: 2024-07-02T00:00:00+09:00
Round trip: True
America/New_York: 2024-07-01T11:00:00-04:00
Round trip: TrueTokyo is already on the next day. astimezone() preserves the instant while changing its local representation. Replacing tzinfo directly keeps the clock fields unchanged, so use conversion when you already know the instant. Supplying an aware UTC value also avoids assumptions about your computer’s local zone.
Distinguish a repeated local time
Save as demo_zoneinfo_fold.py:
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
new_york = ZoneInfo("America/New_York")
wall_time = datetime(2024, 11, 3, 1, 30)
instants = []
for fold in (0, 1):
local = wall_time.replace(tzinfo=new_york, fold=fold)
utc = local.astimezone(timezone.utc)
instants.append(utc)
print(f"fold={fold}: {local.isoformat()} -> {utc.isoformat()}")
print("Fold after round trip:", utc.astimezone(new_york).fold)
elapsed = instants[1] - instants[0]
print("Elapsed minutes:", int(elapsed.total_seconds() / 60))Run it:
python3 demo_zoneinfo_fold.pyOutput:
fold=0: 2024-11-03T01:30:00-04:00 -> 2024-11-03T05:30:00+00:00
Fold after round trip: 0
fold=1: 2024-11-03T01:30:00-05:00 -> 2024-11-03T06:30:00+00:00
Fold after round trip: 1
Elapsed minutes: 60New York repeated 01:30 when clocks moved back. For an ambiguous local time, fold=0 selects the earlier occurrence and fold=1 selects the later one. Conversion from UTC sets the appropriate fold automatically. See ZoneInfo’s fold support.
A local input alone cannot tell you which occurrence someone intended; ask or apply an explicit application policy. We subtract the UTC values to measure elapsed time. Subtracting values with the same tzinfo object ignores their offset and fold differences.
Reject a local time skipped by a clock change
Save as demo_zoneinfo_gap.py:
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
def checked_local_time(wall_time, zone, fold=0):
if wall_time.tzinfo is not None:
raise ValueError("expected a local time without tzinfo")
candidate = wall_time.replace(tzinfo=zone, fold=fold)
returned = candidate.astimezone(timezone.utc).astimezone(zone)
if returned.replace(tzinfo=None) != wall_time:
raise ValueError("local time does not exist")
return candidate
new_york = ZoneInfo("America/New_York")
for hour in (1, 2, 3):
wall_time = datetime(2024, 3, 10, hour, 30)
try:
local = checked_local_time(wall_time, new_york)
print("Accepted:", local.isoformat())
except ValueError as error:
print(f"Rejected {wall_time.isoformat()}: {error}")Run it:
python3 demo_zoneinfo_gap.pyOutput:
Accepted: 2024-03-10T01:30:00-05:00
Rejected 2024-03-10T02:30:00: local time does not exist
Accepted: 2024-03-10T03:30:00-04:00That morning, New York’s clock jumped from 01:59 to 03:00. Attaching a zone does not itself reject the missing 02:30. Our helper checks whether a trip through UTC returns the same local clock reading, and deliberately raises a caught error when it does not. This illustrates the gap behavior described by PEP 495.
The helper expects a datetime without tzinfo and a ZoneInfo zone. It checks existence, but still uses the supplied fold, defaulting to the earlier occurrence for ambiguous input. Choose that fold explicitly when the input can be ambiguous.
Clean up
In the same Bash session, remove only the source files you created, then the empty directory:
rm -- demo_zoneinfo_check.py demo_zoneinfo_convert.py demo_zoneinfo_fold.py demo_zoneinfo_gap.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirIf rmdir reports remaining files, inspect them rather than deleting them blindly.
References
- Python 3.12 zoneinfo documentation
- Python 3.13 zoneinfo documentation
- datetime conversions and arithmetic
- PEP 495: Local Time Disambiguation
Acknowledgements
This post was created with assistance from AI.