I am currently attempting to migrate a code base from using pytz
to using Python's zoneinfo
library. I've run into an issue with how zoneinfo
handles daylight saving time transitions when compared to how pytz
handles them.
Suppose I have a naive datetime
object:
>>> import datetime
>>> start = datetime.datetime(2021, 10, 30, 23, 0)
The approach used for making this object timezone-aware using pytz
was to use localize
, as follows:
>>> local_timezone = pytz.timezone('Europe/Copenhagen')
>>> start_cet = local_timezone.localize(start)
>>> start_cet
datetime.datetime(2021, 10, 30, 23, 0, tzinfo=<DstTzInfo 'Europe/Copenhagen' CEST+2:00:00 DST>)
Notice that start_cet
is very close to the end of CEST 2021. If I add, say, 5 hours onto this time, I get the following:
>>> end = start_cet + datetime.timedelta(hours=5)
>>> end
datetime.datetime(2021, 10, 31, 4, 0, tzinfo=<DstTzInfo 'Europe/Copenhagen' CEST+2:00:00 DST>)
Now, in reality, adding 5 hours on to start_cet
should in fact land us on 3:00 on 2021/10/31, since leaving DST takes us back one hour, moving from CEST to CET. We can get the correct value by applying astimezone
with our pytz
timezone:
>>> end.astimezone(local_timezone)
datetime.datetime(2021, 10, 31, 3, 0, tzinfo=<DstTzInfo 'Europe/Copenhagen' CET+1:00:00 STD>)
Question: What is the equivalent method of finding the actual time when moving through DST when using zoneinfo
?
Here's what I've tried. zoneinfo
has no localize
method, instead we are recommended to simply use replace
:
>>> local_timezone = zoneinfo.ZoneInfo('Europe/Copenhagen')
>>> start_cet = start.replace(tzinfo=local_timezone)
>>> start_cet
datetime.datetime(2021, 10, 30, 23, 0, tzinfo=backports.zoneinfo.ZoneInfo(key='Europe/Copenhagen'))
>>> start_cet.utcoffset()
datetime.timedelta(seconds=7200)
zoneinfo
has the benefit over pytz
that adding a timedelta
will successfully transition us into the correct timezone (as can be seen by the utcoffset
result:
>>> end = start_cet + datetime.timedelta(hours=5)
>>> end
datetime.datetime(2021, 10, 31, 4, 0, tzinfo=backports.zoneinfo.ZoneInfo(key='Europe/Copenhagen'))
>>> end.utcoffset()
datetime.timedelta(seconds=3600)
Despite this, with zoneinfo
timezones there seems to be no way to get what the actual time would have been when adding 5 hours onto start_cet
. As shown above, with pytz
I can use astimezone
, but with zoneinfo
, this does not change anything:
>>> end.astimezone(tz=local_timezone)
datetime.datetime(2021, 10, 31, 4, 0, tzinfo=backports.zoneinfo.ZoneInfo(key='Europe/Copenhagen'))
In addition to @deceze's comment (CET specifies a UTC offset, not a time zone), note that timedelta arithmetic in Python is wall time arithmetic.
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
eu_berlin_tz = ZoneInfo("Europe/Berlin")
start = datetime(2021, 10, 30, 23, 0, tzinfo=eu_berlin_tz)
dur = timedelta(hours=5)
print(start)
# 2021-10-30 23:00:00+02:00 => CEST
print(start + dur)
# 2021-10-31 04:00:00+01:00 => CET
2021-10-30T23:00:00+02:00 to 2021-10-31T04:00:00+01:00 is 5 hours on a wall clock, although a duration of 6 hours passed in reality due to DST becoming inactive.
If you add the duration in UTC and then convert back to the specified time zone, the result is more what you expect IIUC:
def aware_add(dt, duration):
return (dt.astimezone(ZoneInfo("UTC")) + duration).astimezone(dt.tzinfo)
print(aware_add(start, dur))
# 2021-10-31 03:00:00+01:00
2021-10-30T23:00:00+02:00 to 2021-10-31T03:00:00+01:00 is 4 hours on the wall clock but 5 hours in reality.