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
CodePudding user response:
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