I would like to contruct DST-valid timestamps only using the standard library in Python 3.9 and was hoping this was possible with this version.
In my timezone "Europe/Berlin", the DST crossings for 2020 are:
2020-03-29 at 02:00 the clock switches to 03:00 (there is no hour 2!)
2020-10-25 at 03:00 the clock switches back to 02:00 (the hour 2 exists two times!)
My script yields the following output:
MARCH
2020-03-29 01:59:00+01:00 CET plus 1 h: 2020-03-29 02:59:00+01:00 CET
(should be 03:59:00 CEST since there is no hour 2!)
OCTOBER
2020-10-25 02:00:00+02:00 CEST plus 1 h: 2020-10-25 03:00:00+01:00 CET
(seems OK- EDIT: should be 02:00 CET!!!)
Example code is provided below. Windows user may need to "pip install tzdata" to make it work.
Any advise would be greatly appreciated!
'''
Should work out of the box with Python 3.9
Got a fallback import statement.
BACKPORT (3.6+)
pip install backports.zoneinfo
WINDOWS (TM) needs:
pip install tzdata
'''
from datetime import datetime, timedelta
from time import tzname
try:
from zoneinfo import ZoneInfo
except ImportError:
from backports import zoneinfo
ZoneInfo = zoneinfo.ZoneInfo
tz = ZoneInfo("Europe/Berlin")
hour = timedelta(hours=1)
print("MARCH")
dt_01 = datetime(2020, 3, 29, 1, 59, tzinfo=tz)
dt_02 = dt_01 + hour
print(f"{dt_01} {dt_01.tzname()} plus 1 h: {dt_02} {dt_02.tzname()}")
print("\nOCTOBER")
dt_01 = datetime(2020, 10, 25, 2, 0, tzinfo=tz)
dt_02 = dt_01 + hour
print(f"{dt_01} {dt_01.tzname()} plus 1 h: {dt_02} {dt_02.tzname()}")
Although it is counter-intuitive, this is as expected. See this blog post for more details on how datetime arithmetic works. The reason for this is that adding a timedelta
to a datetime
should be thought of as "advance the calendar/clock by X amount" rather than "what will the calendar/clock say after this amount of time has elapsed". Note that the first question might result in a time that doesn't even occur in the local time zone!
If you want, "What datetime
represents what time it will be after the amount of time represented by this timedelta
has elapsed?" (which it seems you do), you should do something equivalent to converting to UTC and back, like so:
from datetime import datetime, timedelta, timezone
def absolute_add(dt: datetime, td: timedelta) -> datetime:
utc_in = dt.astimezone(timezone.utc) # Convert input to UTC
utc_out = utc_in + td # Do addition in UTC
civil_out = utc_out.astimezone(dt.tzinfo) # Back to original tzinfo
return civil_out
I believe you can create a timedelta
subclass that overrides __add__
to do this for you (I'd kind of like to introduce something like this to the standard library if I can).
Note that if dt.tzinfo
is None
, this will use your system local time zone to determine how to do absolute addition, and it will return an aware time zone. Running this in America/New_York
:
>>> absolute_add(datetime(2020, 11, 1, 1), timedelta(hours=1))
datetime.datetime(2020, 11, 1, 1, 0, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=68400), 'EST'))
If you want this to do civil addition for naïve datetimes and absolute addition for aware datetimes, you can check whether or not it's naïve in the function:
def absolute_add_nolocal(dt: datetime, td: timedelta) -> datetime:
if dt.tzinfo is None:
return dt + td
return absolute_add(dt, td)
Also, to be clear, this is not something to do with zoneinfo
. This has always been the semantics of datetimes in Python, and it wasn't something we could change in a backwards-compatible way. pytz
does work a little differently, because adding pytz
-aware datetimes does the wrong thing, and requires a normalize
step after the arithmetic has occurred, and the pytz
author decided that normalize
should use absolute-time semantics.
absolute_add
also works with pytz
and dateutil
, since it uses operations that work well for all time zone libraries.