I want to know if there's a daylight saving time change within the next hours. Thereby I recognized an unexpected dependency between datetime.timedelta, astimezone and daylight saving time. During the daylight saving time change the timedelta calculation seems no to work. Here's a simple example:
import datetime
import pytz
format = '%Y-%m-%d %H:%M:%S %Z%z'
local_tz = pytz.timezone('Europe/Berlin')
time_diff = datetime.timedelta(hours = 1)
print((datetime.datetime(2023, 2, 26, 1, 0, 0) + time_diff).astimezone(local_tz).strftime(format))
# => 2023-02-26 02:00:00 CET+0100
print((datetime.datetime(2023, 3, 26, 1, 0, 0) + time_diff).astimezone(local_tz).strftime(format))
# => 2023-03-26 01:00:00 CET+0100
I would expect that the last print would result "2023-03-26 02:00:00 CET+0100" or "2023-03-26 03:00:00 CEST+0200". Does anyone can explain this behaviour?
After different tries I found a solution by adding the time delta after adding the timezone to the timestamp.
print((datetime.datetime(2023, 3, 26, 1, 0, 0).astimezone(local_tz) + time_diff).strftime(format))
But I still don't understand the error in my first used code.
My versions:
- Python 3.10.2
- pytz 2022.7
See also this answer by Paul Ganssle - with native Python's timedelta combined with time zones, non-existing datetimes (like 2023-02-26 02:00:00 CET+0100) have to be expected.
Here's a slightly extended comparison for reference, + comments in the code. pytz is deprecated since the release of Python 3.9's zoneinfo.
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
import pandas as pd
import pytz
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
# -----
# in vanilla Python, we can create non-existent datetime...
# I) tz already set
t = datetime(2023, 3, 26, 1, tzinfo=ZoneInfo("Europe/Berlin"))
print(t + timedelta(hours=1))
# 2023-03-26 02:00:00+01:00
# this datetime should not exist in that time zone since there is a DST transtion,
# 1 am UTC+1 plus one hour gives 3 am UTC+2
# II) tz set after timedelta addition
print(datetime(2023, 3, 26, 1) + timedelta(hours=1))
# 2023-03-26 02:00:00
# this is ok since no tz specified
print((datetime(2023, 3, 26, 1) + timedelta(hours=1)).replace(tzinfo=ZoneInfo("Europe/Berlin")))
# 2023-03-26 02:00:00+01:00
# again, we have a non-existent datetime
print((datetime(2023, 3, 26, 1) + timedelta(hours=1)).astimezone(ZoneInfo("Europe/Berlin")))
# 2023-03-26 01:00:00+01:00
# also a bit confusing; 2 am would be non-existing, so the hour is "corrected"
# backwards before setting the time zone
# -----
# adding the timedelta in UTC as "absolute duration" works as expected:
print(absolute_add(t, timedelta(hours=1)))
# 2023-03-26 03:00:00+02:00
# with pytz timezones, you can normalize to correct the non-existent datetime:
tz = pytz.timezone("Europe/Berlin")
t = tz.localize(datetime(2023, 3, 26, 1))
print(tz.normalize(t + timedelta(hours=1)))
# 2023-03-26 03:00:00+02:00
# this is correctly implemented in pandas for instance:
t = pd.Timestamp(2023, 3, 26, 1).tz_localize("Europe/Berlin")
print(t + pd.Timedelta(hours=1))
# 2023-03-26 03:00:00+02:00