datetime, zoneinfo & time
Store instants in UTC with timezone-aware datetime. Convert for display with zoneinfo (tzdata on Windows via tzdata package). Use timedelta for durations, not integer seconds alone.
Recipe
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
utc_now = datetime.now(timezone.utc)
local = utc_now.astimezone(ZoneInfo("America/New_York"))
print(utc_now.isoformat(), local.isoformat())When to reach for this:
- API timestamps (always UTC ISO-8601)
- User-local display formatting
- Scheduling across DST boundaries
- Measuring elapsed time (
monotonicfor perf) - Parsing ISO strings from JSON
Working Example
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
def next_billing(from_utc: datetime, days: int) -> datetime:
if from_utc.tzinfo is None:
raise ValueError("aware datetime required")
return from_utc + timedelta(days=days)
def format_for_user(instant_utc: datetime, tz_name: str) -> str:
tz = ZoneInfo(tz_name)
return instant_utc.astimezone(tz).strftime("%Y-%m-%d %H:%M %Z")
start = datetime(2026, 7, 9, 12, 0, tzinfo=timezone.utc)
due = next_billing(start, 30)
print(format_for_user(due, "Europe/Berlin"))What this demonstrates:
- Reject naive datetimes in domain functions
- Arithmetic with
timedeltapreserves awareness ZoneInfoIANA names for correct DST- strftime for display only - store ISO in data
Deep Dive
Aware vs Naive
| Type | Safe for |
|---|---|
| Aware (tz set) | Storage, APIs |
| Naive | Local wall clock only with care |
Perf timing
time.perf_counter()for benchmarks- Not
datetime.nowfor durations
Gotchas
- Naive now() comparisons - DST bugs. Fix:
datetime.now(timezone.utc). - Integer epoch everywhere - loses subsecond and tz context. Fix: datetime in models, epoch at boundary if needed.
- zoneinfo missing on Windows - install
tzdataPyPI. Fix: document in deployment. - Parsing ambiguous local times - DST fold. Fix: store UTC; convert on display only.
- strftime %Z misleading - abbreviations ambiguous. Fix: include offset
%zin APIs.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| dateutil | Complex recurrence | Simple timedelta enough |
| pendulum | Friendly API preference | Stdlib-only policy |
| epoch int in DB | Legacy schema | Greenfield APIs |
FAQs
UTC storage mandatory?
Yes for distributed systems - convert at UI edge.
fromisoformat?
Parses many ISO-8601 forms in 3.11+ - prefer for JSON timestamps.
How to test DST?
Use fixed aware datetimes in tests - not now().
monotonic vs time.time?
perf_counter monotonic for durations; time.time wall clock subject to NTP jumps.
SQLAlchemy DateTime?
Store timezone-aware or UTC normalized - match DB column type.
Leap seconds?
Python datetime ignores leap seconds - same as most systems; use UTC.
Replace pytz?
zoneinfo is preferred in 3.9+ new code.
Parse HTTP dates?
email.utils.parsedate_to_datetime for RFC 7231 headers.
Schedule daily job?
Use aware next-run calculator or APScheduler - watch DST for local time triggers.
Pydantic datetime?
Coerces ISO strings; enforce aware with validators in v2.
Related
- json & Serialization - ISO in JSON
- Standard Library Overview - map
- hashlib, secrets & uuid - time-based ids (uuid1 caution)
- Standard Library Best Practices - time rules
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.