Back to Blog

Time zones in Python — datetime, pytz, and zoneinfo

April 12, 2026
3 min read
Written by ConvertTime Team
pythontime-zonesdevelopersdatetimezoneinfo
Python's time zone handling went through a major modernization with the introduction of `zoneinfo` in Python 3.9. Before that, `pytz` was the standard third-party library for time zones. The two work differently in subtle ways, and modern Python code should use `zoneinfo`. For converting Unix timestamps to/from Python datetimes, the [epoch converter](/calculators/epoch) handles them. ## Naive vs aware datetimes Python's `datetime` objects come in two flavors: **Naive datetime**: no time zone information. Just date + time. ```python from datetime import datetime dt = datetime(2026, 5, 21, 14, 30, 0) dt.tzinfo # None — naive ``` **Aware datetime**: includes time zone information. ```python from datetime import datetime, timezone dt = datetime(2026, 5, 21, 14, 30, 0, tzinfo=timezone.utc) dt.tzinfo # ``` Naive datetimes are dangerous because they're ambiguous. Two naive datetimes can't be compared meaningfully across time zones. Always use aware datetimes for anything that crosses zones. ## Modern: zoneinfo (Python 3.9+) ```python from datetime import datetime from zoneinfo import ZoneInfo # Create a time-zone-aware datetime dt = datetime(2026, 5, 21, 14, 30, 0, tzinfo=ZoneInfo("America/New_York")) print(dt) # 2026-05-21 14:30:00-04:00 (EDT, UTC-4) # Convert to another zone tokyo = dt.astimezone(ZoneInfo("Asia/Tokyo")) print(tokyo) # 2026-05-22 03:30:00+09:00 # Get current UTC now_utc = datetime.now(tz=ZoneInfo("UTC")) # Get current in any zone now_tokyo = datetime.now(tz=ZoneInfo("Asia/Tokyo")) ``` `zoneinfo` is part of the standard library starting in Python 3.9. It uses the system's IANA tz database (or `tzdata` on Windows). It handles DST transitions correctly. ## The legacy: pytz Before Python 3.9, `pytz` was the standard. Lots of existing code uses it: ```python import pytz from datetime import datetime # pytz uses .localize() instead of tzinfo= parameter ny_tz = pytz.timezone("America/New_York") dt = ny_tz.localize(datetime(2026, 5, 21, 14, 30, 0)) # Convert to another zone tokyo_tz = pytz.timezone("Asia/Tokyo") tokyo_dt = dt.astimezone(tokyo_tz) ``` `pytz` was a community-maintained library that handled time zones before Python had built-in support. It works but has a different API than what Python 3.9+ supports. ## Why migrate from pytz to zoneinfo Several reasons: **1. Built-in.** No external dependency. `from zoneinfo import ZoneInfo` is always available in Python 3.9+. **2. Standard API.** Uses `tzinfo=` parameter consistently with other Python time zone APIs. **3. Better DST handling.** `zoneinfo` handles ambiguous times (the duplicate hour in fall-back) more cleanly than pytz. **4. Active maintenance.** `pytz` is in maintenance mode; `zoneinfo` gets ongoing improvements. **5. Faster.** Generally 2-5× faster than pytz for typical operations. ## Migrating pytz code to zoneinfo ```python # Before (pytz) import pytz ny = pytz.timezone("America/New_York") dt = ny.localize(datetime(2026, 5, 21, 14, 30, 0)) # After (zoneinfo) from zoneinfo import ZoneInfo dt = datetime(2026, 5, 21, 14, 30, 0, tzinfo=ZoneInfo("America/New_York")) ``` The big difference: `pytz` has `.localize()`; `zoneinfo` uses `tzinfo=` directly in the constructor. The behavior is otherwise the same. For complex codebases, you can run them side-by-side during migration. Both APIs work in the same Python. ## Common patterns ### Get current time in UTC ```python from datetime import datetime, timezone now = datetime.now(timezone.utc) ``` ### Get current time in a specific zone ```python from datetime import datetime from zoneinfo import ZoneInfo tokyo_now = datetime.now(ZoneInfo("Asia/Tokyo")) ``` ### Convert between zones ```python ny_dt = datetime(2026, 5, 21, 14, 30, 0, tzinfo=ZoneInfo("America/New_York")) tokyo_dt = ny_dt.astimezone(ZoneInfo("Asia/Tokyo")) ``` ### Parse ISO 8601 with time zone ```python from datetime import datetime dt = datetime.fromisoformat("2026-05-21T14:30:00+05:30") print(dt.tzinfo) # +05:30 offset ``` ### Format with time zone ```python dt.isoformat() # "2026-05-21T14:30:00+05:30" dt.strftime("%Y-%m-%d %H:%M %Z") # Includes time zone name ``` ## DST handling with zoneinfo ```python from datetime import datetime from zoneinfo import ZoneInfo # Spring forward (US): March 8, 2026, 2:00 AM doesn't exist # Trying to create it: try: dt = datetime(2026, 3, 8, 2, 30, 0, tzinfo=ZoneInfo("America/New_York")) print(dt.utcoffset()) # zoneinfo handles this gracefully except Exception as e: print(f"Error: {e}") # Fall back (US): November 1, 2026, 1:30 AM happens twice # zoneinfo's fold attribute lets you specify which: dt_first = datetime(2026, 11, 1, 1, 30, 0, tzinfo=ZoneInfo("America/New_York"), fold=0) dt_second = datetime(2026, 11, 1, 1, 30, 0, tzinfo=ZoneInfo("America/New_York"), fold=1) print(dt_first.utcoffset()) # -04:00 (EDT) print(dt_second.utcoffset()) # -05:00 (EST) ``` The `fold` attribute (Python 3.6+) handles ambiguous times. `fold=0` means "the earlier occurrence" (still in DST); `fold=1` means "the later occurrence" (after DST ends). ## Working with naive datetimes Sometimes you have to interact with naive datetimes (legacy code, old APIs): ```python from datetime import datetime from zoneinfo import ZoneInfo # Convert naive (assumed local) to aware naive_local = datetime(2026, 5, 21, 14, 30, 0) aware = naive_local.replace(tzinfo=ZoneInfo("America/New_York")) # Convert naive (assumed UTC) to aware UTC naive_utc = datetime(2026, 5, 21, 14, 30, 0) aware_utc = naive_utc.replace(tzinfo=ZoneInfo("UTC")) # Convert aware to naive (drops tzinfo, dangerous) naive = aware.replace(tzinfo=None) ``` Always know whether your naive datetime represents UTC, local time, or some specific zone. Without that knowledge, naive datetimes are bug-prone. ## Common mistakes **Comparing naive and aware datetimes**: ```python naive = datetime(2026, 5, 21) aware = datetime(2026, 5, 21, tzinfo=ZoneInfo("UTC")) naive == aware # TypeError ``` **Assuming `datetime.now()` returns UTC**: ```python datetime.now() # Returns LOCAL naive datetime — bug-prone datetime.now(timezone.utc) # Returns aware UTC datetime — correct ``` **Hardcoding offsets**: ```python # Wrong — doesn't handle DST ny_dt = utc_dt - timedelta(hours=5) # Assumes EST forever # Right — uses real time zone ny_dt = utc_dt.astimezone(ZoneInfo("America/New_York")) ``` **Storing time zones as strings without using IANA names**: ```python # Bad zone = "EST" # Ambiguous (Indian Standard Time? Eastern?) # Good zone = "America/New_York" # IANA name, unambiguous ``` ## Available time zones ```python import zoneinfo zones = zoneinfo.available_timezones() print(len(zones)) # ~600 zones print("America/New_York" in zones) # True ``` The IANA tz database is the source. ~600 zones globally. ## Time zone-aware libraries Several Python libraries help with time zones: **arrow**: friendlier API on top of datetime. ```python import arrow now = arrow.now('Asia/Tokyo') ``` **pendulum**: another wrapper, focused on usability. ```python import pendulum dt = pendulum.now('Asia/Tokyo') ``` **maya**: parses natural language dates. ```python import maya dt = maya.when('next Tuesday at 3 PM EST').datetime() ``` For new projects, prefer the standard library (datetime + zoneinfo) unless you have a specific need that the wrappers solve. ## Performance `zoneinfo` is fast enough for most use cases. For high-frequency time zone operations: - Cache `ZoneInfo` instances rather than creating each time - Use `fromtimestamp(..., tz=ZoneInfo(...))` for performance over manual conversion - For very high-frequency: consider arrow or pendulum which can be faster for some patterns ## FAQ ### Should I use pytz or zoneinfo for new code? `zoneinfo` for Python 3.9+. It's the standard library, faster, and uses the standard `tzinfo=` API. ### Can I migrate gradually from pytz to zoneinfo? Yes — they coexist in the same Python. Write new code with zoneinfo; refactor pytz code as you touch it. ### What about Python 2? Python 2 is end-of-life. If you're stuck with it, use pytz. Migration to Python 3.9+ should be the priority. ### How does zoneinfo handle DST? Correctly, using the IANA tz database. The `fold` attribute resolves ambiguous times. ### Can I use zoneinfo without `tzdata`? On most Linux/macOS systems, yes — they include tzdata. On Windows, you may need to `pip install tzdata`. ### What's the right way to store dates in a database? Always store UTC. Convert to user's zone for display. PostgreSQL `timestamptz` does this automatically. ## Bottom line Use `zoneinfo` (Python 3.9+) for time zones. It's the standard library, fast, and uses the Python 3 idiomatic API. Migrate from `pytz` when you can. Always work with aware datetimes; naive datetimes are bug-prone. For converting Unix timestamps to/from Python datetimes, the [epoch converter](/calculators/epoch) handles them.

Share this article

Related Articles