Dates, Times, and Timezones

Updated

September 7, 2026

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.py

Output:

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.py

Output:

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

  1. Write a function that calculates how many days, hours, and minutes remain until January 1, 2030, 00:00:00 UTC.
  2. Given a timestamp string formatted as "07/Sep/2026:14:00:00 +0000", parse it into an aware datetime object using datetime.strptime.
  3. Demonstrate comparing two timestamps from different timezones (Asia/Tokyo and Europe/London) and prove that Python accurately evaluates which event occurred first.
  4. 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: datetime and zoneinfo documentation.