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.