Two Different DST Fall-Back Instants Compare Equal in datetime (tested)

Once a year, local clocks in many timezones go back an hour, so a wall-clock time like 01:30 happens twice. We measured what Python's datetime does when you build both occurrences with zoneinfo and compare them: they are genuinely an hour apart in real time, but == says they're equal.

1. Building both occurrences of the ambiguous hour

from datetime import datetime
from zoneinfo import ZoneInfo

tz = ZoneInfo("America/New_York")
# 2026-11-01: US DST fall-back date, clocks go 1:59:59 -> 1:00:00
first = datetime(2026, 11, 1, 1, 30, 0, tzinfo=tz)       # fold=0 (default)
second = first.replace(fold=1)                            # the later, repeated 1:30

print(first.utcoffset(), second.utcoffset())
print(first.astimezone(ZoneInfo("UTC")))
print(second.astimezone(ZoneInfo("UTC")))
-1 day, 20:00:00 -1 day, 19:00:00
2026-11-01 05:30:00+00:00
2026-11-01 06:30:00+00:00

Measured: the fold attribute (PEP 495) picks which side of the DST transition 01:30 means. Converted to UTC, first and second are a full hour apart — they are different real-world instants, and .astimezone() gets this right.

2. Comparing them directly

print("== :", first == second)
print("< :", first < second)
print("hash equal:", hash(first) == hash(second))
print("subtract:", second - first)
== : True
< : False
hash equal: True
subtract: 0:00:00

Measured: first == second is True, their hashes are equal, and subtracting one from the other gives timedelta(0) — even though they are an hour apart in UTC, as section 1 just showed. No exception, no warning. Aware datetime comparison and arithmetic ignore fold entirely; only .astimezone() (and .utcoffset()) take it into account.

3. Why this is dangerous in real code

This only matters during the ~1 hour per year each DST-observing timezone falls back, but code that stores or compares local-time timestamps — a log line, a booking slot, an "events happened in this order" check — can silently treat two distinct events that occurred an hour apart as the same moment, or compute a zero duration between them, during that window.

4. Fix: compare in UTC, or convert before comparing

first_utc = first.astimezone(ZoneInfo("UTC"))
second_utc = second.astimezone(ZoneInfo("UTC"))
print("== in UTC:", first_utc == second_utc)
print("subtract in UTC:", second_utc - first_utc)
== in UTC: False
subtract in UTC: 1:00:00

Measured: converting both to UTC before comparing gives the correct answer — not equal, one hour apart. The safe pattern confirmed here: store and compare timestamps in UTC (or any fixed, non-DST-observing offset), and only convert to a local, DST-observing zone for display.

Frequently asked questions

Do two different DST fall-back instants really compare as equal in Python?

Yes -- we measured datetime.datetime == returning True, hash() matching, and subtraction giving timedelta(0) for two zoneinfo-aware instants that are a full hour apart in real UTC time.

Does this raise any exception or warning?

No -- in our test it compared, hashed and subtracted silently with no indication the two values represent different real-world moments.

What is the fold attribute?

Per PEP 495 and the datetime docs, fold (0 or 1) disambiguates which occurrence of a repeated local wall-clock time a naive-looking local datetime means. We confirmed .astimezone() and .utcoffset() respect it, but == and subtraction between aware datetimes do not.

How do I compare timestamps safely across a DST transition?

We confirmed converting both datetimes to UTC (or any fixed-offset zone) with .astimezone() before comparing or subtracting gives the correct, non-ambiguous result.

Does this only affect timezones that observe daylight saving time?

Yes -- it only happens during the fall-back transition in DST-observing zones like America/New_York. A fixed-offset zone like UTC has no repeated wall-clock hour, so this ambiguity can't occur there.

Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in datetime and zoneinfo modules under TZ=America/New_York, constructing the two local-time instants that exist during the 2026-11-01 DST fall-back transition (01:30 occurs twice) using the documented fold attribute, then comparing them with ==, <, hash() and subtraction. PlainHeaders is our extension; Python's datetime and zoneinfo modules are built in.