You always want to convert
datetimeobjects to UTC before comparing them.
Why is that, you ask?
I am sure there are plenty of other reasons, but we recently discovered a serious footgun at work:
Comparing “timezone-based” datetime objects fails during DST transition!
I will explain this using the DST transition from CEST to CET in Germany (the is the transition on the last Sunday in October, from “Sommerzeit” to “Winterzeit”, where wall clocks are turned back one hour), so our timezone is "Europe/Berlin".
The underlying problem is that in this example, the hour between 02:00 and 02:59 exists twice on wall clocks - the first time while still in CEST, and then a second tim in CET after the transition.
This means that we cannot know which point in real time “02:30 local time in timezone Europe/Berlin on the 26th of October 2025” maps to - we also need to specify whether we are in the “first pass” (CEST) or in the “second pass” (CET) over that repeating hour.
To help visualize this, take a look at this image i blatantly stole from Philipe Fatio:

In python, the answer to the question “are we in the first pass (before transition) or in the second pass (after transistion)” is stored in the fold attribute of a datetime object.
datetime.foldIn
[0, 1]. Used to disambiguate wall times during a repeated interval. (A repeated interval occurs when clocks are rolled back at the end of daylight saving time or when the UTC offset for the current zone is decreased for political reasons.) The values 0 and 1 represent, respectively, the earlier and later of the two moments with the same wall time representation.
With that additional piece of information, it is now possible to exactly map to a single point in UTC:
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
europe_berlin = ZoneInfo("Europe/Berlin")
dt1 = datetime(2025, 10, 26, 2, 30, tzinfo=europe_berlin)
dt2 = datetime(2025, 10, 26, 2, 30, tzinfo=europe_berlin, fold=1)
print(dt1) # 2025-10-26 02:30:00+02:00
print(dt2) # 2025-10-26 02:30:00+01:00
print(dt1.astimezone(timezone.utc)) # 2025-10-26 00:30:00+00:00
print(dt2.astimezone(timezone.utc)) # 2025-10-26 01:30:00+00:00
So if we can map from a “timezone-datetime” to a “UTC-datetime”, why does this break during the DST transition? Shouldn’t the comparison work now?
One would assume so, the docs even state:
datetimeobjects are equal if they represent the same date and time, taking into account the time zone.
BUT, they also say:
datetimeinstances in a repeated interval are never equal to datetime instances in other time zone.
Looking at this example:
from datetime import datetime, timezone
from pprint import pprint
from zoneinfo import ZoneInfo
europe_berlin = ZoneInfo("Europe/Berlin")
# before the transition
dt0 = datetime(2025, 10, 26, 1, 30, tzinfo=europe_berlin)
# during the transition - "first pass"
dt1 = datetime(2025, 10, 26, 2, 30, tzinfo=europe_berlin)
# during the transition - "second pass"
dt2 = datetime(2025, 10, 26, 2, 30, tzinfo=europe_berlin, fold=1)
# after the transition
dt3 = datetime(2025, 10, 26, 3, 30, tzinfo=europe_berlin)
print(dt0) # 2025-10-26 01:30:00+02:00
print(dt1) # 2025-10-26 02:30:00+02:00
print(dt2) # 2025-10-26 02:30:00+01:00
print(dt3) # 2025-10-26 03:30:00+01:00
print(dt0.astimezone(timezone.utc)) # 2025-10-25 23:30:00+00:00
print(dt1.astimezone(timezone.utc)) # 2025-10-26 00:30:00+00:00
print(dt2.astimezone(timezone.utc)) # 2025-10-26 01:30:00+00:00
print(dt3.astimezone(timezone.utc)) # 2025-10-26 02:30:00+00:00
print(dt0 == dt0.astimezone(timezone.utc)) # True
print(dt1 == dt1.astimezone(timezone.utc)) # False
print(dt2 == dt2.astimezone(timezone.utc)) # False
print(dt3 == dt3.astimezone(timezone.utc)) # True
print(dt0 < dt1) # True
print(dt0 < dt2) # True
print(dt0 < dt3) # True
print(dt1 < dt2) # False
print(dt1 < dt3) # True
print(dt2 < dt3) # True
Given the wording from the docs, I understand why dt2 and its UTC equivalent are not equal (because dt2.fold is 1), but I would expect dt1 to be equal to its UTC equivalent (because dt1.fold is the default value of 0).
And to be honest I still don’t understand why that is not the case.
Anyway, I have learned my lesson and will only compare UTC-based datetime objects :)
Further reading: