datetime arithmetic example code

These functions were originally written for Python 3.6+, then updated for 3.8+ and were not tested with other versions.

Functions implementing wall and absolute time semantics

from datetime import timedelta, timezone
UTC = timezone.utc

def wall_add(dt, other):
    return dt + other

def wall_sub(dt, other):
    if isinstance(other, timedelta):
        return wall_add(dt, -1 * other)

    return dt.replace(tzinfo=None)- other.replace(tzinfo=None)


def absolute_add(dt, other):
    return (dt.astimezone(UTC) + other).astimezone(dt.tzinfo)

def absolute_sub(dt, other):
    if isinstance(other, timedelta):
        return absolute_add(dt, -1 * other)

    return (dt.astimezone(UTC) - other.astimezone(UTC))

datetime subclasses with absolute or wall time semantics

from datetime import datetime, timedelta, timezone

UTC = timezone.utc

from functools import total_ordering


@total_ordering
class AbsoluteDateTime(datetime):
    """A version of datetime that uses only elapsed time semantics"""

    _utc_datetime_cache = None

    @property
    def _utc_datetime(self):
        if self._utc_datetime_cache is None:
            dt = datetime(
                self.year,
                self.month,
                self.day,
                self.hour,
                self.minute,
                self.second,
                self.microsecond,
                tzinfo=self.tzinfo,
                fold=self.fold,
            )
            self._utc_datetime_cache = dt.astimezone(UTC)

        return self._utc_datetime_cache

    def __add__(self, other):
        # __add__ is only supported between datetime and timedelta
        dt = datetime.__add__(self._utc_datetime, other)
        if self.tzinfo is not UTC:
            dt = dt.astimezone(self.tzinfo)

            # Required to support the case where tzinfo is None
            dt = dt.replace(tzinfo=self.tzinfo)
        return type(self).as_absolute_datetime(dt)

    def __sub__(self, other):
        if isinstance(other, timedelta):
            # Use __add__ implementation if it's datetime and timedelta
            return self + (-1) * other
        else:
            return datetime.__sub__(self._utc_datetime, other.astimezone(UTC))

    def __eq__(self, other):
        return datetime.__eq__(self._utc_datetime, other.astimezone(UTC))

    def __lt__(self, other):
        return datetime.__lt__(self._utc_datetime, other.astimezone(UTC))

    @classmethod
    def as_absolute_datetime(cls, dt):
        """Construct an AbsoluteDatetime from any datetime subclass"""
        return cls(
            *dt.timetuple()[0:6],
            microsecond=dt.microsecond,
            tzinfo=dt.tzinfo,
            fold=dt.fold
        )


@total_ordering
class WallDateTime(datetime):
    """A version of datetime that uses only wall time semantics"""

    def __add__(self, other):
        # __add__ is only supported between datetime and timedelta
        dt = datetime.__add__(self.replace(tzinfo=None), other)

        if self.tzinfo is not None:
            dt = dt.replace(tzinfo=self.tzinfo)

        return self.__class__.as_wall_datetime(dt)

    def __sub__(self, other):
        if isinstance(other, timedelta):
            # Use __add__ implementation if it's datetime and timedelta
            return self + (-1) * other
        else:
            return datetime.__sub__(
                self.replace(tzinfo=None), other.replace(tzinfo=None)
            )

    def __eq__(self, other):
        return datetime.__eq__(
            self.replace(tzinfo=None), other.replace(tzinfo=None)
        )

    def __lt__(self, other):
        return datetime.__lt__(
            self.replace(tzinfo=None), other.replace(tzinfo=None)
        )

    @classmethod
    def as_wall_datetime(cls, dt):
        """Construct a WallDateTime from any datetime subclass"""
        return cls(
            *dt.timetuple()[0:6],
            microsecond=dt.microsecond,
            tzinfo=dt.tzinfo,
            fold=dt.fold
        )