python/cpython · error · TypeError

cannot mix naive and timezone-aware time

Error message

cannot mix naive and timezone-aware time

What it means

datetime.__sub__ computes a base timedelta from wall-clock differences, then adjusts for utcoffset when the two tzinfos differ. If either utcoffset() returns None (a naive datetime mixed against an aware one under differing tzinfo), the subtraction is undefined and raises TypeError('cannot mix naive and timezone-aware time').

Source

Thrown at Lib/_pydatetime.py:2372

            if isinstance(other, timedelta):
                return self + -other
            return NotImplemented

        days1 = self.toordinal()
        days2 = other.toordinal()
        secs1 = self._second + self._minute * 60 + self._hour * 3600
        secs2 = other._second + other._minute * 60 + other._hour * 3600
        base = timedelta(days1 - days2,
                         secs1 - secs2,
                         self._microsecond - other._microsecond)
        if self._tzinfo is other._tzinfo:
            return base
        myoff = self.utcoffset()
        otoff = other.utcoffset()
        if myoff == otoff:
            return base
        if myoff is None or otoff is None:
            raise TypeError("cannot mix naive and timezone-aware time")
        return base + otoff - myoff

    def __hash__(self):
        if self._hashcode == -1:
            if self.fold:
                t = self.replace(fold=0)
            else:
                t = self
            tzoff = t.utcoffset()
            if tzoff is None:
                self._hashcode = hash(t._getstate()[0])
            else:
                days = _ymd2ord(self.year, self.month, self.day)
                seconds = self.hour * 3600 + self.minute * 60 + self.second
                self._hashcode = hash(timedelta(days, seconds, self.microsecond) - tzoff)
        return self._hashcode

    # Pickle support.

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Make the naive side aware with its true zone: naive.replace(tzinfo=timezone.utc)
  2. Or strip awareness after converting: aware.astimezone(timezone.utc).replace(tzinfo=None)
  3. For durations, compare .timestamp() values instead of subtracting objects
  4. Fix custom tzinfo subclasses so utcoffset() never returns None for aware use

Example fix

// before
age = datetime.now(timezone.utc) - created_at_naive

# after
age = datetime.now(timezone.utc) - created_at_naive.replace(tzinfo=timezone.utc)
Defensive patterns

Strategy: validation

Validate before calling

from datetime import datetime, timezone

def duration_between(a: datetime, b: datetime) -> float:
    return a.timestamp() - b.timestamp()  # naive assumed local; aware exact

Type guard

from datetime import datetime

def both_same_awareness(a, b) -> bool:
    return (a.tzinfo is None) == (b.tzinfo is None)

Try / catch

try:
    delta = a - b
except TypeError:
    delta = a.replace(tzinfo=None) - b.replace(tzinfo=None)  # wall-clock fallback

Prevention

When it happens

Trigger: aware_dt - naive_dt or naive_dt - aware_dt where the naive side has tzinfo None; also aware datetimes whose custom tzinfo.utcoffset() returns None (a naive-behaving tzinfo) mixed with real ones.

Common situations: Computing durations between DB naive timestamps and current aware time: now(timezone.utc) - row['created_at']; custom tzinfo subclasses that return None from utcoffset; mixed ORM sessions with and without timezone support.

Related errors


AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14). Data as JSON: /api/errors/6ed54d5338807496. Report an issue: GitHub.