🚀 Supercharge your YouTube channel's growth with AI.
Try YTGrowAI FreePython pytz: Migrate to zoneinfo

Updating timezone code in a Python app means checking that its dates still mean what you intend. pytz is a separately installed package for working with time zones, while zoneinfo is Python’s built-in option in Python 3.9 and later.
This article shows how to map pytz calls to zoneinfo and compare their behavior at daylight-saving changes.
TL;DR
Use zoneinfo.ZoneInfo for new Python 3.9+ code, and keep pytz where older runtimes or dependencies still require it. Migration changes how local times and arithmetic work.
- pytz is a separately installed package.
- pytz uses localize() for a naive local datetime and normalize() after local-time arithmetic.
- zoneinfo attaches its IANA zone when you construct an aware datetime and uses fold to distinguish repeated times.
- Use UTC for elapsed-time calculations. choose local wall-time arithmetic for recurring calendar events.
What Is pytz in Python?
pytz is a Python package that provides timezone objects based on the IANA timezone database. A timezone-aware datetime carries a tzinfo object, so Python can interpret its offset.
These two APIs can use the same key, such as America/New_York, but their datetime rules differ. I use that zone below because its seasonal offset change makes the migration behavior visible. A fixed-offset zone would hide the differences.
How to Migrate from pytz to zoneinfo
This walkthrough replaces the provider and its pytz-specific methods, then checks how the application defines “one day later.”
Step 1: Select the timezone provider
Install it in the application environment with ./.venv/bin/python -m pip install pytz.
Step 2: Replace pytz timezone construction
from datetime import datetime
import pytz
ny_pytz = pytz.timezone("America/New_York")
legacy = ny_pytz.localize(datetime(2024, 1, 15, 9, 0))
from zoneinfo import ZoneInfo
ny_zoneinfo = ZoneInfo("America/New_York")
current = datetime(2024, 1, 15, 9, 0, tzinfo=ny_zoneinfo)
The values represent 9:00 AM in New York on the stated date. Avoid passing a pytz region as tzinfo directly: for many regions, that can select a historical local-mean-time offset instead of the desired current rule.
Step 3: Convert an aware datetime with astimezone()
Use astimezone() to display the same instant in another region.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
instant = datetime(2024, 7, 1, 12, 0, tzinfo=timezone.utc)
local = instant.astimezone(ZoneInfo("America/New_York"))
Convert deliberately when crossing between modules.
Step 4: Run the migration example through a DST transition
Run a named source file to compare pytz and zoneinfo at both a repeated clock time and a spring-forward boundary. The example deliberately prints fixed dates so its output does not depend on when you run it.
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
import pytz
zone_name = "America/New_York"
naive = datetime(2024, 11, 3, 1, 30)
legacy_zone = pytz.timezone(zone_name)
legacy_early = legacy_zone.localize(naive, is_dst=True)
legacy_late = legacy_zone.localize(naive, is_dst=False)
print("pytz first occurrence:", legacy_early.strftime("%Y-%m-%d %H:%M %Z%z"))
print("pytz second occurrence:", legacy_late.strftime("%Y-%m-%d %H:%M %Z%z"))
new_zone = ZoneInfo(zone_name)
new_early = naive.replace(tzinfo=new_zone, fold=0)
new_late = naive.replace(tzinfo=new_zone, fold=1)
print("zoneinfo first occurrence:", new_early.strftime("%Y-%m-%d %H:%M %Z%z"))
print("zoneinfo second occurrence:", new_late.strftime("%Y-%m-%d %H:%M %Z%z"))
start = legacy_zone.localize(datetime(2024, 3, 9, 13))
legacy_next = legacy_zone.normalize(start + timedelta(days=1))
new_start = datetime(2024, 3, 9, 13, tzinfo=new_zone)
new_next = new_start + timedelta(days=1)
absolute_next = (new_start.astimezone(timezone.utc) + timedelta(days=1)).astimezone(new_zone)
print("pytz plus one day, normalized:", legacy_next.strftime("%Y-%m-%d %H:%M %Z%z"))
print("zoneinfo same wall time next day:", new_next.strftime("%Y-%m-%d %H:%M %Z%z"))
print("zoneinfo 24 elapsed hours:", absolute_next.strftime("%Y-%m-%d %H:%M %Z%z"))
Run it from the directory containing the saved file:
./.venv/bin/python timezone_migration_demo.py
The executed output is:
pytz first occurrence: 2024-11-03 01:30 EDT-0400
pytz second occurrence: 2024-11-03 01:30 EST-0500
zoneinfo first occurrence: 2024-11-03 01:30 EDT-0400
zoneinfo second occurrence: 2024-11-03 01:30 EST-0500
pytz plus one day, normalized: 2024-03-10 14:00 EDT-0400
zoneinfo same wall time next day: 2024-03-10 13:00 EDT-0400
zoneinfo 24 elapsed hours: 2024-03-10 14:00 EDT-0400

A fall-back clock hour repeats. pytz selects an occurrence with is_dst, while zoneinfo uses fold.
What Changes at Daylight-Saving Boundaries?
| Situation | pytz | zoneinfo | Application choice |
|---|---|---|---|
| Attach a region to local fields | localize(value) | tzinfo=ZoneInfo(key) | Use the user’s region |
| Repeated clock time | is_dst=True or False | fold=0 or 1 | Choose an occurrence |
| Skipped clock time | is_dst=None raises | Validate the local input | Reject or resolve by policy |
| Arithmetic across a transition | normalize() after addition | Review the resulting offset | Choose elapsed or wall time |
For ambiguous input, pytz can raise by passing is_dst=None to localize(). ZoneInfo accepts either fold value for a repeated hour.
For user input, convert the proposed local reading to UTC and back to its region.
List and select timezone keys
pytz exposes all_timezones and all_timezones_set for available keys, plus common_timezones and common_timezones_set for a shorter selection. The list forms can populate a selector, and the set forms support membership tests. Sort the displayed choices for predictable navigation.
country_names maps ISO country codes to names, while country_timezones maps a code to a list of keys. A country can return several regions, so treat the mapping as a filter and let the user choose the location that matches the feature. Store the selected IANA key for later use.
Provide timezone data in deployments
ZoneInfo reads the system timezone database and can use the first-party tzdata package when the system source is absent. Add tzdata as an application dependency when the deployment environment does not provide timezone data. Test a key such as America/New_York in the same container that runs the application.
Install pytz for legacy code
pytz remains a separately installed package. Use the package interpreter to install it in the project environment: ./.venv/bin/python -m pip install pytz. This keeps the dependency tied to the interpreter that runs the application.
PyPI also provides a source archive for build systems that retain or mirror source inputs. Application projects can use pip and record the resulting dependency in their project configuration. Check the selected interpreter if a package command reports that pip is absent.
Step 5: Preserve the zone key with scheduled local times
Store a completed event as a UTC instant. For a future appointment, retain its local date, clock time and region key.
ZoneInfo serializes a named zone by its key. A process that reads the value needs timezone data for that key, and its database version may differ from the writer’s.
pytz Installation and Package Sources
Install pytz into the environment that runs the application. A different interpreter may not see the package, so invoke pip through the project interpreter.
- Configured package index: use the project interpreter and dependency file.
- Official source archive: point the build at the local archive and keep its origin.
- Legacy setuptools process: follow it only when the project requires that build path.
For a virtual environment named .venv, the installation command is ./.venv/bin/python -m pip install pytz. The -m form runs pip as a module of that exact interpreter, which ties installation to the environment. The PyPI project page also publishes a source distribution for build systems that consume tarballs.
For a tarball build, download the source archive from the official pytz release page and point pip at the local archive from the project environment. This suits build systems that mirror source files or control which artifacts enter a build. Keep the archive in the approved artifact store and record its origin.
Using setuptools: older project instructions may describe setuptools or setup.py as the installation interface. Current projects should generally let pip resolve and install the package from the configured index or a supplied archive. If a legacy build script requires setuptools, follow that project’s supported build process rather than invoking setup.py install as a new deployment method.
After installation, check the interpreter and import in the environment that will run the application. A dependency declaration also matters: record pytz in the project’s dependency file so a fresh environment reproduces the install. If the code has moved to zoneinfo and no dependency still imports pytz, remove the package only after checking the full application and its dependencies.
Convert Between Timezones
Convert an aware datetime with astimezone(). It preserves the represented instant and changes the local clock fields and offset for the destination region. Start with a known instant, commonly a UTC timestamp, when converting a stored event for display.
For pytz, pass pytz.timezone(key) to astimezone(). For zoneinfo, pass ZoneInfo(key). Choose an IANA region such as Europe/Paris because an abbreviation such as CET may be ambiguous and does not describe changing regional rules.
| Operation | Guidance |
|---|---|
| Choose destination | Use pytz.timezone(key) or ZoneInfo(key) |
| Preserve instant | Convert an aware datetime with astimezone() |
| Assign intended region | Use replace(tzinfo=…) only for known naive local fields |
Use astimezone() to convert an already-aware value. Attaching a new region with replace(tzinfo=…) changes its interpretation without converting the instant. Reserve replace(tzinfo=…) for naive fields whose intended region is known.
UTC storage separates an event’s instant from its display. When a user in another region opens the event, convert that instant to the user’s zone at display time. A future appointment is different: its intended local clock time can matter more than preserving a fixed number of elapsed seconds, so store its local date, local time and region key as the scheduling rule.
Choosing a Timezone from pytz
pytz exposes IANA keys through all_timezones and common_timezones. The first contains the available names from its timezone data, while the second offers a smaller set suitable for common choices. Use the names as identifiers, not as display labels, and sort choices before presenting them to users.
Membership checks can use all_timezones_set or common_timezones_set, which are set forms of the corresponding lists. A form should validate a submitted key against the supported set before constructing a timezone. That rejects typos and unsupported values at the input boundary instead of failing later in date processing.
For a country-oriented selector, country_names maps codes to names and country_timezones maps codes to zone keys. Offer a region choice when the exact locality matters, then persist that IANA key. A country code alone cannot determine local time everywhere in a country.
Keep the display label separate from the stored key. Labels can include a city and a current offset for clarity, but the offset can change when daylight-saving rules or government decisions change. Resolve the offset from the named zone when showing the choice.
- all_timezones lists the available timezone keys.
- common_timezones offers a shorter set for common choices.
- country_timezones filters region keys by country code.
Common Timezone Migration Edge Cases
The migration risk is a change in meaning, not just a renamed method. Check whether each datetime represents an instant, a local reading, or a recurring schedule before replacing its timezone calls.
| Case | What can happen | Safer response |
|---|---|---|
| Repeated local time | A clock reading occurs twice during a backward offset change | Choose the occurrence explicitly with fold for zoneinfo |
| Missing local time | A clock reading is skipped during a forward offset change | Reject it or apply a documented scheduling policy |
| Elapsed duration | Local clock arithmetic crosses an offset change | Convert to UTC, add the duration, then convert back |
| Recurring wall time | A fixed elapsed duration shifts the intended local hour | Recompute the next local occurrence from its region key |
| Missing timezone database | ZoneInfo cannot find a requested key in the runtime | Provide system timezone data or the tzdata package |
The same wall-clock fields can map to two instants during a repeated hour. zoneinfo fold=0 selects the earlier occurrence and fold=1 the later one. pytz exposes is_dst, a daylight-saving choice that does not map cleanly to fold for every historical offset change.
A skipped local reading has no corresponding instant. pytz rejects it when localize() uses is_dst=None. With zoneinfo, round-trip the local fields through UTC and apply a policy if they change.
Duration and calendar arithmetic answer different questions. For a day of elapsed time, add to the UTC instant. For a 9 AM appointment tomorrow, advance the local date and resolve that time under the region’s rules.
ZoneInfo obtains IANA timezone data from the operating system and can use the first-party tzdata package when system data is unavailable. Minimal containers can omit that database. Add tzdata as an application dependency when the deployment needs it, and test the actual zone keys in the same runtime image used in production.
Jurisdictions revise timezone rules, so machines can resolve a zone key with different database versions. Keep the UTC instant for completed events. For a future schedule, keep the local fields and region key, then define how a rule change affects its resolved instant.
Conclusion: Choose the Timezone API for Your Runtime
pytz remains a practical choice for code that depends on its interface, but zoneinfo is the standard-library option. Test the migration on the dates and schedules your application supports.
- Python zoneinfo documentation
- pytz project documentation
- Convert UTC to EST in Python
- Find the difference between two dates in Python
FAQ
These quick answers cover pytz installation, Python’s built-in timezone support and the pytz-to-zoneinfo decision.
Is pytz included in Python?
No. pytz is a separate package installed from PyPI. Python 3.9 and later include zoneinfo in the standard library.
How do I install pytz?
Install it in the active project environment with python -m pip install pytz.
Should I replace pytz with zoneinfo?
For new code on Python 3.9 or later, zoneinfo is the standard-library option. Keep pytz when a supported runtime or dependency requires its API, and test DST and arithmetic behavior before migrating existing code.


