TypeError: can't compare offset-naive and offset-aware datetimes (tested)

TypeError: can't compare offset-naive and offset-aware datetimes means one datetime has a timezone and the other doesn't. We measured where each comes from and the fix that doesn't silently shift your times, on Python 3.13.5.

What raises it

A datetime is naive if tzinfo is None and aware if it carries an offset. Python refuses to order or subtract a mix of the two, because it can't know what zone the naive one is in.

from datetime import datetime, timezone
naive = datetime(2026, 10, 9, 12, 0)
aware = datetime(2026, 10, 9, 12, 0, tzinfo=timezone.utc)
try:
    print(naive < aware)
except TypeError as e:
    print("TypeError:", e)
try:
    print(aware - naive)
except TypeError as e:
    print("TypeError:", e)
print("== :", naive == aware)

Output:

TypeError: can't compare offset-naive and offset-aware datetimes
TypeError: can't subtract offset-naive and offset-aware datetimes
== : False

Measured on Python 3.13.5: < and - raise TypeError, but == does not: it silently returns False, even though both values say 12:00. An equality check between a naive and an aware datetime is always false, so if ts == deadline can quietly never fire.

2. Where the naive one usually comes from

from datetime import datetime, timezone
# the usual culprits: utcnow() and now() are naive, fromisoformat("...Z") is aware
print(datetime.utcnow().tzinfo)
print(datetime.now().tzinfo)
print(datetime.fromisoformat("2026-10-09T12:00:00Z").tzinfo)
print(datetime.fromisoformat("2026-10-09T12:00:00").tzinfo)
print(datetime.now(timezone.utc).tzinfo)

Output:

/tmp/nv/b2.py:3: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
  print(datetime.utcnow().tzinfo)
None
None
UTC
None
UTC

Measured: datetime.utcnow() and datetime.now() return naive values (and utcnow() is deprecated on 3.12+, with the warning shown). fromisoformat() returns aware only when the string has an offset or Z. So comparing utcnow() against a parsed API timestamp like ...Z is the classic trigger.

3. Fix: replace() vs astimezone()

from datetime import datetime, timezone
naive = datetime(2026, 10, 9, 12, 0)   # meant as UTC
# WRONG for UTC-meaning values: astimezone() assumes the naive value is LOCAL time
print(naive.astimezone(timezone.utc))
# RIGHT: attach the zone without shifting the clock
print(naive.replace(tzinfo=timezone.utc))

Output:

2026-10-09 16:00:00+00:00
2026-10-09 12:00:00+00:00

Measured with TZ=America/New_York: calling astimezone() on a naive value treats it as local time and shifts it, so a value you meant as 12:00 UTC becomes 16:00 UTC, 4 hours off with no error. If the naive value is already UTC, use replace(tzinfo=timezone.utc), which attaches the zone without moving the clock. Use astimezone() only when the naive value really is local time. More on that trap in astimezone on a naive datetime.

4. Prevent it: make everything aware

from datetime import datetime, timezone
deadline = datetime.fromisoformat("2026-10-09T12:00:00+00:00")
now = datetime.now(timezone.utc)          # aware, not utcnow()
print(type(now < deadline).__name__)
# a set/dict mixing both silently treats them as different keys
s = {datetime(2026, 1, 1), datetime(2026, 1, 1, tzinfo=timezone.utc)}
print(len(s))

Output:

bool
2

Create times with datetime.now(timezone.utc) and parse with offsets, and comparisons just work. Measured: a set holding the naive and aware versions of the same instant keeps both (length 2), so mixed values also break de-duplication and dict lookups silently.

Frequently asked questions

How do I fix can't compare offset-naive and offset-aware datetimes?

Make both sides aware. If the naive value is UTC, use dt.replace(tzinfo=timezone.utc); create new times with datetime.now(timezone.utc) instead of utcnow(). We measured both on Python 3.13.5.

Should I use astimezone() or replace(tzinfo=...)?

replace() attaches a zone without changing the clock. astimezone() on a naive value assumes local time and shifts it; we measured 12:00 meant as UTC becoming 16:00 UTC under America/New_York.

Why does == between naive and aware datetimes not raise an error?

Equality is defined to return False for a naive/aware mix instead of raising. We measured the same 12:00 instant comparing unequal, so equality checks fail silently.

Is datetime.utcnow() naive?

Yes. We measured tzinfo None, and on Python 3.12+ it emits a DeprecationWarning. Use datetime.now(timezone.utc).

Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5 on Linux (TZ=America/New_York for the astimezone block): naive vs aware ordering, subtraction and equality, where naive values come from, replace vs astimezone, and set de-duplication. Every block was run verbatim and its output pasted from that run. PlainHeaders is our extension; everything else here is the standard library.