Naive vs Aware Datetimes
Python's datetime objects come in two flavors. A naive datetime has no timezone information — it's just a clock reading with no context. An aware datetime carries timezone information and represents an unambiguous point in time. Always prefer aware datetimes in production code.
from datetime import datetime, timezone
# Naive — dangerous, avoid in production
naive = datetime(2024, 3, 1, 14, 0, 0)
print(naive.tzinfo) # None
# Aware (UTC) — safe
aware = datetime(2024, 3, 1, 14, 0, 0, tzinfo=timezone.utc)
print(aware.tzinfo) # UTC
The Modern Way: zoneinfo (Python 3.9+)
Since Python 3.9, the standard library includes zoneinfo, which reads the IANA timezone database directly. This is the recommended approach for new code:
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
# Create a timezone-aware datetime in Seoul time
seoul_tz = ZoneInfo("Asia/Seoul")
dt = datetime(2024, 3, 1, 14, 0, tzinfo=seoul_tz)
print(dt) # 2024-03-01 14:00:00+09:00
# Convert to UTC
utc_dt = dt.astimezone(timezone.utc)
print(utc_dt) # 2024-03-01 05:00:00+00:00
# Get current time in a specific timezone
now_in_tokyo = datetime.now(tz=ZoneInfo("Asia/Tokyo"))
print(now_in_tokyo)
# On Python 3.8 or earlier, install tzdata package separately:
# pip install tzdata (provides IANA database on Windows)
Legacy: pytz
Before zoneinfo, pytz was the standard. It has a quirk: you must use localize() instead of passing tzinfo directly, or DST handling will be wrong:
import pytz
from datetime import datetime
tz = pytz.timezone("America/New_York")
# WRONG — do not use this pattern with pytz
dt_wrong = datetime(2024, 3, 1, 14, 0, tzinfo=tz)
# CORRECT — use localize()
dt_correct = tz.localize(datetime(2024, 3, 1, 14, 0))
print(dt_correct) # 2024-03-01 14:00:00-05:00
# Converting between timezones
utc_dt = dt_correct.astimezone(pytz.utc)
tokyo_dt = utc_dt.astimezone(pytz.timezone("Asia/Tokyo"))
Working with Django
# settings.py
USE_TZ = True
TIME_ZONE = 'UTC'
# In views/models — use django.utils.timezone
from django.utils import timezone
now = timezone.now() # timezone-aware UTC datetime
localtime = timezone.localtime(now) # convert to settings.TIME_ZONE
Common Pitfalls
- DST transitions: America/New_York is UTC-5 in winter and UTC-4 in summer. Use the IANA timezone name, not a fixed offset.
- Mixing naive and aware: Python raises
TypeErrorwhen you try to compare or subtract a naive and aware datetime — a helpful error that prevents silent bugs. - Serialization: When converting datetime to a string for JSON or databases, always use
.isoformat()which includes the UTC offset.