Dates, Times, and Timezones
Dates, Times, and Timezones
After reading this chapter, you will master Python’s temporal primitives (datetime, date, timedelta), perform strict IANA timezone calculations using zoneinfo, parse and format ISO-8601 strings, and avoid daylight saving time (DST) calculation traps.
Mental model
In Python, a datetime object is either: - Naive: Lacks timezone context (tzinfo=None). Ambiguous and dangerous in distributed systems. - Aware: Explicitly carries timezone context via zoneinfo.ZoneInfo.
Naive Datetime (Dangerous in Production):
datetime(2026, 9, 7, 10, 0, 0) ──▶ Unanchored in physical time
Aware Datetime (Production Standard):
datetime(2026, 9, 7, 10, 0, 0, tzinfo=ZoneInfo("America/New_York"))
│
▼ (Lossless conversion across any world timezone)
UTC Epoch Timestamp: 1788789600.0
│
▼
datetime(2026, 9, 7, 14, 0, 0, tzinfo=ZoneInfo("UTC"))
Always store and transmit timestamps in UTC. Only convert to local regional timezones at the user interface presentation layer.
Minimal example
Save as timezone_mechanics.py:
# timezone_mechanics.py
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
def main() -> None:
# 1. Capture current time in UTC (Always use timezone.utc, never naive datetime.now())
utc_now = datetime.now(timezone.utc)
print(f"Current UTC time : {utc_now.isoformat()}")
# 2. Timezone conversion with zoneinfo
tokyo_tz = ZoneInfo("Asia/Tokyo")
tokyo_time = utc_now.astimezone(tokyo_tz)
print(f"Converted to Tokyo : {tokyo_time.strftime('%Y-%m-%d %H:%M:%S %Z')}")
ny_tz = ZoneInfo("America/New_York")
ny_time = utc_now.astimezone(ny_tz)
print(f"Converted to New York : {ny_time.strftime('%Y-%m-%d %H:%M:%S %Z')}")
# 3. Arithmetic with timedelta
maintenance_window = timedelta(hours=4, minutes=30)
scheduled_end = utc_now + maintenance_window
print(f"Maintenance ends at : {scheduled_end.isoformat()}")
# 4. Fast ISO-8601 parsing (Python 3.11+ handles full ISO-8601 specifications)
parsed_dt = datetime.fromisoformat("2026-09-07T14:30:00+09:00")
print(f"Parsed ISO string : {parsed_dt} (TZ: {parsed_dt.tzinfo})")
if __name__ == "__main__":
main()Run via uv run python timezone_mechanics.py:
Current UTC time : 2026-09-07T...Z
Converted to Tokyo : 2026-09-07 ... JST
Converted to New York : 2026-09-07 ... EDT
Maintenance ends at : 2026-09-07T...Z
Parsed ISO string : 2026-09-07 14:30:00+09:00 (TZ: ...+09:00)
Worked examples
Case 1: The Daylight Saving Time (DST) Arithmetic Trap
Adding a timedelta to a local wall-clock time during a DST transition can yield an incorrect wall-clock reading because hours are skipped or repeated:
# dst_safe_math.py
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
def demonstrate_dst_jump() -> None:
tz = ZoneInfo("America/New_York")
# 2026 Spring Forward: 2:00 AM jumps to 3:00 AM
before_dst = datetime(2026, 3, 8, 1, 30, tzinfo=tz)
print(f"Before DST transition: {before_dst}")
# Add 1 hour in UTC (Physical time elapsed)
utc_version = before_dst.astimezone(ZoneInfo("UTC"))
utc_after = utc_version + timedelta(hours=1)
ny_after = utc_after.astimezone(tz)
print(f"1 hour later in NY : {ny_after} (Notice jump from 1:30 EST to 3:30 EDT!)")
if __name__ == "__main__":
demonstrate_dst_jump()Run:
uv run python dst_safe_math.pyOutput:
Before DST transition: 2026-03-08 01:30:00-05:00
1 hour later in NY : 2026-03-08 03:30:00-04:00 (Notice jump from 1:30 EST to 3:30 EDT!)
Case 2: TTL Expiration and Token Age Validation
Validating session tokens or cache TTLs requires monotonic or UTC-anchored comparison:
# token_expiration.py
from datetime import datetime, timedelta, timezone
def is_token_expired(issued_at_iso: str, ttl_seconds: int) -> bool:
issued_at = datetime.fromisoformat(issued_at_iso)
now = datetime.now(timezone.utc)
age = now - issued_at
return age > timedelta(seconds=ttl_seconds)
if __name__ == "__main__":
current = datetime.now(timezone.utc)
valid_token = current.isoformat()
old_token = (current - timedelta(hours=2)).isoformat()
print("Checking token validity (TTL: 3600 seconds):")
print(f" Valid token expired? {is_token_expired(valid_token, 3600)}")
print(f" Old token expired? {is_token_expired(old_token, 3600)}")Run:
uv run python token_expiration.pyOutput:
Checking token validity (TTL: 3600 seconds):
Valid token expired? False
Old token expired? True
Pitfalls
Pitfall 1: Calling datetime.now() Without Arguments
Calling datetime.now() produces a naive datetime object using local system time without timezone metadata. Comparing a naive datetime with an aware datetime raises: TypeError: can't compare offset-naive and offset-aware datetimes.
Always call datetime.now(timezone.utc).
Pitfall 2: Using Deprecated datetime.utcnow()
datetime.utcnow() returns a naive datetime representing UTC time, making it impossible to convert to other timezones safely. datetime.utcnow() is officially deprecated in Python 3.12+. Use datetime.now(timezone.utc) instead.
Exercises
- Write a function that calculates how many days, hours, and minutes remain until January 1, 2030, 00:00:00 UTC.
- Given a timestamp string formatted as
"07/Sep/2026:14:00:00 +0000", parse it into an aware datetime object usingdatetime.strptime. - Demonstrate comparing two timestamps from different timezones (
Asia/TokyoandEurope/London) and prove that Python accurately evaluates which event occurred first. - Calculate the start and end timestamps for the current calendar month in UTC.
Further reading
- PEP 615: Support for the IANA Time Zone Database in the Standard Library (
zoneinfo). - Python Standard Library:
datetimeandzoneinfodocumentation.