Python using timedelta around DST hour

Question:

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.

Asked By: teuneboon

||

Answers:

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

# or using UTC:
print(
      (second_date.astimezone(ZoneInfo("UTC")) - 
       first_date.astimezone(ZoneInfo("UTC"))).total_seconds()
      )
# 3000.0
Answered By: FObersteiner
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.