AI Skill Report Card

Handling Timezone Aware Datetimes

A-86·Aug 15, 2026·Source: Web

Handling Timezone-Aware Datetimes

14 / 15
Python
from datetime import datetime from zoneinfo import ZoneInfo # Always construct timezone-aware datetimes with ZoneInfo dt = datetime(2024, 11, 3, 1, 30, tzinfo=ZoneInfo("America/New_York")) print(dt) # 2024-11-03 01:30:00-04:00 (or -05:00 depending on fold) # Convert between timezones utc_dt = dt.astimezone(ZoneInfo("UTC")) # Get current time in a zone now = datetime.now(ZoneInfo("Europe/London"))

Never use pytz-style localize() patterns or naive datetime.replace(tzinfo=...) for arithmetic-sensitive code — zoneinfo uses PEP 495 fold instead.

Recommendation
Fix the odd dead code in Example 1 (the `if False else` ternary is confusing and should be removed)
13 / 15

Progress:

  • Identify whether the datetime is a fixed instant (use UTC/ZoneInfo("UTC")) or a "wall clock" time in a location (use IANA zone key)
  • Construct with ZoneInfo(key) passed directly to tzinfo=, never via .replace() after the fact for DST-sensitive calculations
  • Check for ambiguous/imaginary times if the datetime falls near a DST transition
  • Convert with .astimezone(), not manual offset math
  • Verify tzdata source (system tz database vs tzdata pip package) for deployment consistency
  • Cache ZoneInfo instances if creating many (they're already cached internally per-process, but be aware of zoneinfo.reset_tzpath() invalidation)
Recommendation
Add an example showing correct disambiguation resolution using `fold` explicitly rather than just detecting ambiguity

Zone keys are IANA names like "America/New_York", "Asia/Kolkata", "Etc/UTC" — never use fixed abbreviations like "EST" (ambiguous and not DST-aware).

Ambiguous times: when clocks fall back, a wall-clock time occurs twice.

Python
from zoneinfo import ZoneInfo from datetime import datetime # 1:30 AM on Nov 3, 2024 occurs twice in America/New_York dt1 = datetime(2024, 11, 3, 1, 30, tzinfo=ZoneInfo("America/New_York"), fold=0) # first occurrence (EDT, -04:00) dt2 = datetime(2024, 11, 3, 1, 30, tzinfo=ZoneInfo("America/New_York"), fold=1) # second occurrence (EST, -05:00)

Imaginary times: when clocks spring forward, some wall-clock times never occur.

Python
# 2:30 AM on Mar 10, 2024 doesn't exist in America/New_York (clocks jump 2:00 -> 3:00) dt = datetime(2024, 3, 10, 2, 30, tzinfo=ZoneInfo("America/New_York")) # zoneinfo will still produce a result, silently treating it per the `fold` rule — # never assume such a datetime is "real"; validate against transition boundaries # if imaginary times must be rejected

Arithmetic pitfall: adding a timedelta to a timezone-aware datetime does wall-clock arithmetic, not absolute-time arithmetic. To do correct elapsed-time math across a DST boundary, convert to UTC first.

Python
start = datetime(2024, 3, 9, 12, 0, tzinfo=ZoneInfo("America/New_York")) end = start + timedelta(days=1) # naive: assumes 24 real hours passed — WRONG across DST correct_end = (start.astimezone(ZoneInfo("UTC")) + timedelta(days=1)).astimezone(ZoneInfo("America/New_York"))
17 / 20

Example 1: Input: Schedule a recurring 9 AM meeting in Europe/Berlin for the next 5 days, storing as UTC for a database. Output:

Python
from datetime import datetime, timedelta from zoneinfo import ZoneInfo berlin = ZoneInfo("Europe/Berlin") utc = ZoneInfo("UTC") meetings_utc = [] base_date = datetime(2024, 3, 29, 9, 0, tzinfo=berlin) for i in range(5): local_dt = base_date.replace(day=base_date.day + i) if False else \ datetime(2024, 3, 29 + i, 9, 0, tzinfo=berlin) meetings_utc.append(local_dt.astimezone(utc)) # Result correctly reflects the DST jump on Mar 31 (Berlin springs forward)

Example 2: Input: A user reports their app shows the wrong time exactly during DST changeover in America/Sao_Paulo. Output: Check if the datetime is ambiguous or imaginary by comparing UTC offsets before/after:

Python
from zoneinfo import ZoneInfo from datetime import datetime, timedelta tz = ZoneInfo("America/Sao_Paulo") dt = datetime(2019, 2, 16, 23, 30, tzinfo=tz) offset_before = dt.utcoffset() offset_after = (dt + timedelta(hours=1)).utcoffset() if offset_before != offset_after: # near a transition — use fold to disambiguate, or reject if imaginary pass

Example 3: Input: Deploying to a minimal Docker container that lacks /usr/share/zoneinfo. Output: Add the tzdata package (pip install tzdata) as a dependency — zoneinfo falls back to it automatically when the system tz database is unavailable. Don't hardcode paths; let zoneinfo handle source resolution.

Recommendation
Include a brief example of validating/rejecting imaginary times rather than just noting zoneinfo 'silently treats' them
  • Store instants in UTC; store local wall-clock intent (e.g., "9 AM every day") with the IANA key separately, and compute UTC at read time — this correctly handles future DST rule changes.
  • Always pass ZoneInfo via tzinfo= at construction time; avoid .replace(tzinfo=...) for anything that will do date arithmetic.
  • Use .astimezone() for conversions, never manual UTC-offset math.
  • Depend on the tzdata PyPI package explicitly for portability (Windows has no system tz database at all).
  • Use zoneinfo.available_timezones() to validate user-supplied zone keys before construction.
  • Using deprecated 3-letter abbreviations ("EST", "PST") — not supported as zone keys and inherently ambiguous.
  • Assuming timedelta arithmetic on aware datetimes is DST-safe — it's wall-clock arithmetic, not elapsed real-time.
  • Forgetting fold when a wall-clock time is ambiguous — default fold=0 picks the first (earlier UTC offset in a fall-back) occurrence, which may not be what's intended.
  • Mixing pytz and zoneinfo in the same codebase — pytz requires localize()/normalize() idioms incompatible with zoneinfo's PEP 495 model.
  • Not bundling tzdata for environments without a system timezone database (bare containers, Windows) — leads to ZoneInfoNotFoundError in production only.
0
Grade A-AI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
14/15
Workflow
13/15
Examples
17/20
Completeness
18/20
Format
15/15
Conciseness
13/15