I have a script that receives regular pings, and whenever the last ping is more than 10 minutes ago it throws an error and goes into panic mode. Simplified example:
from datetime import datetime
import time
from random import randint
class Checker:
def __init__(self):
self.last_ping = datetime.now()
def ping(self):
self.last_ping = datetime.now()
def panic_if_stale(self):
if (datetime.now() - self.last_ping).total_seconds() > 600:
# no ping for longer than 10 minutes, we panic
raise Exception('PANIC')
if __name__ == '__main__':
checker = Checker()
while True:
if randint(0, 10) == 5:
# this is not actually random, just simulating
checker.ping()
checker.panic_if_stale()
time.sleep(60)
Last sunday daylight savings time changed over here, and I had a bug(the .total_seconds()
line was .seconds
, which made the bug actually trigger a panic, but there's still weird behavior even without that). After some experimenting I found out that the following is happening:
from datetime import datetime, timedelta
if __name__ == '__main__':
first_date = datetime(2022, 10, 30, 2, 30)
# this should be 50 minutes later, but after the time change:
second_date = datetime(2022, 10, 30, 2, 20, fold=1)
# this prints out -600 seconds:
print((second_date - first_date).total_seconds())
# this prints out 3000 seconds(the correct answer in my opinion):
print(second_date.timestamp() - first_date.timestamp())
# this prints out 85800 seconds, but I used the wrong function so that's kind of whatever:
print((second_date - first_date).seconds)
# this prints out "False", even though it should be True:
print((second_date - first_date) > timedelta(minutes=5))
Does timedelta not keep DST into account even though datetime obviously does(as seen by the timestamp difference). Is it not best practice to use timedelta where possible and should I just use timestamps(feels kind of ugly to me)? Preferably I wouldn't want to add an external dependency to this project like pytz.
CodePudding user response:
A way to avoid DST issues: use UTC. Ex:
from datetime import datetime, timezone
class Checker:
def __init__(self):
self.last_ping = datetime.now(timezone.utc)
def ping(self):
self.last_ping = datetime.now(timezone.utc)
def panic_if_stale(self):
if (datetime.now(timezone.utc) - self.last_ping).total_seconds() > 600:
# no ping for longer than 10 minutes, we panic
raise Exception('PANIC')
A bit of background, why DST transitions can cause issues when combined with timedelta arithmetic: timedelta arithmetic is wall time arithmetic. It does not consider fold
. The durations are the same as you would see on a clock that is adjusted to the DST transition. See also Semantics of timezone-aware datetime arithmetic.
Using aware datetime in the example makes this a bit more clear:
from zoneinfo import ZoneInfo
first_date = datetime(2022, 10, 30, 2, 30, fold=0, tzinfo=ZoneInfo("Europe/Berlin"))
second_date = datetime(2022, 10, 30, 2, 20, fold=1, tzinfo=ZoneInfo("Europe/Berlin"))
print(first_date, second_date)
# 2022-10-30 02:30:00 02:00 2022-10-30 02:20:00 01:00
# WALL TIME: -600 seconds, 02:30:00 h -> 02:20:00 h
print((second_date - first_date).total_seconds())
# -600.0
# still correct; .timestamp considers fold attribute:
print(second_date.timestamp() - first_date.timestamp())
# 3000.0