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.