Skip to content
Python Basics: Time Zones and Clock Changes with zoneinfo

Python Basics: Time Zones and Clock Changes with zoneinfo

October 13, 2026

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 1

Save 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.py

Successful 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.py

Output:

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: True

Tokyo 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.py

Output:

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: 60

New 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.py

Output:

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

That 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_dir

If rmdir reports remaining files, inspect them rather than deleting them blindly.

References

Acknowledgements

This post was created with assistance from AI.

Last updated on