FULL PYTHON FIX · 9 MIN READ
Fix can't compare offset-naive and offset-aware datetimes
A datetime without timezone information cannot be ordered against one with an offset. Identify what the naive value means, make that interpretation explicit, and verify comparisons across offsets.
Quick fix
Suppose the data contract states that the offset-free deadline is in UTC.
from datetime import datetime, timezone
deadline_utc_naive = datetime(2026, 10, 13, 10, 0)
deadline_utc = deadline_utc_naive.replace(tzinfo=timezone.utc)
event_utc = datetime(2026, 10, 13, 9, 0, tzinfo=timezone.utc)
print(event_utc < deadline_utc)Video guide: Fix can't compare offset-naive and offset-aware datetimes
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
We have an event at nine in the morning with a UTC timezone, and a deadline at ten with no timezone information. It is tempting to compare the hours and say the event came first. Run the expression. Python raises TypeError because it cannot order an offset-aware datetime against an offset-naive one. The event identifies an instant through its UTC offset. The deadline supplies a date and clock time but no offset, so it does not yet tell us which instant it represents. Ten in UTC and ten in another region could be hours apart. The error protects us from silently treating those wall-clock values as if they shared a timezone.
- 00:42
A common quick patch is to remove the timezone from the aware value, but that merely hides the missing information. We need to inspect both objects and identify the source of the deadline. In this demonstration, the external data contract explicitly says the offset-free deadline was recorded in UTC. That fact makes a careful repair possible. If the source did not give us a timezone, we would need to obtain one rather than guess. The ISO strings reveal the contrast. The deadline has no offset suffix, and tzinfo prints None. The event has plus zero hours and tzinfo prints UTC.
- 01:22
In Python terminology the deadline is naive and the event is aware. Naive does not automatically mean local time or UTC; it means the object does not supply usable timezone information. Its meaning must come from the source system's contract. Maybe a database documented that all offset-free timestamps are UTC. Maybe a user entered a local meeting time in a named region. Those are different cases. If we wrongly label a New York wall-clock time as UTC, we shift the represented instant by several hours without changing the displayed hour. We should also distinguish attaching a timezone from converting between timezones.
- 02:05
Attaching metadata to a naive value states what that existing clock reading meant. Converting an already-aware value keeps the instant and can change the displayed clock reading. We will show both operations, beginning with the known-UTC deadline. Because the source contract says the deadline's clock reading is UTC, we can attach timezone dot UTC to that naive datetime. The replace call does not convert ten into another hour. It labels the same ten o'clock reading as UTC. Both datetimes are now aware and have a consistent interpretation, so the event at nine is before the deadline at ten.
- 02:46
The output is True. This repair would be wrong if the original deadline actually meant ten o'clock in a different timezone. In that case, we would need to attach the correct source zone and then convert or compare. Do not solve the TypeError by simply adding UTC everywhere without checking provenance. For a new application, it is often simpler to capture aware timestamps from the start and store them with an offset, so this ambiguity never enters the comparison path. When you receive older naive data, write down exactly why your code interprets it as a particular zone. That rationale is part of the function's contract, not just an implementation detail.
- 03:28
These two aware datetimes show different clock readings: nine in UTC and eleven in a fixed plus-two offset. They represent the same instant. Equality prints True, and less-than prints False because an instant is not earlier than itself. The final line converts the aware plus-two value to UTC and displays nine with the UTC offset. Unlike replace, astimezone performs a conversion while preserving the instant. This is a crucial test: code that simply compares the displayed hours would get it wrong. We also need to test the failure policy. In the guide's checked task, a function called happened before must raise ValueError when either input is naive rather than invent a timezone.
- 04:15
Real geographic zones can have daylight-saving transitions and ambiguous local times; fixed offsets here keep the terminal demonstration deterministic. If your production input is a named region, use a real timezone such as one from zoneinfo and define how ambiguous times are handled. The example focuses on the general rule: establish trustworthy timezone meaning before you order two datetimes. Write happened before to accept two aware datetimes and return whether the event occurred before the cutoff. Check each input's UTC offset. If either has no usable offset, raise ValueError with the requested timezone required message.
- 04:58
Otherwise convert both aware inputs to UTC and compare. The checker tests an earlier event, equivalent instants using plus-two and UTC, and a naive event that must be rejected. This exercise deliberately refuses to guess because a naive value could mean UTC, a local zone, or something else. In the earlier repaired example we had a documented UTC source, so attaching UTC was justified. These two designs are consistent: use known provenance when you have it, and demand it when you do not. When you next see the TypeError about offset-naive and offset-aware datetimes, inspect isoformat and tzinfo on both operands.
- 05:41
Identify the source contract. Use replace only to label a naive clock reading whose timezone is known, and use astimezone to convert an already-aware instant. Then test two representations of the same instant to confirm the comparison is about time, not the printed hour.
Compare a naive deadline with an aware event
An event arrives with a UTC timestamp, while a deadline was created without tzinfo. Run the short comparison. Python raises TypeError: can't compare offset-naive and offset-aware datetimes. One datetime identifies an instant on a timeline through its UTC offset; the other is only a calendar date and wall-clock time until the program says what timezone it represents.
The error can arrive from parsed text, a database field, `datetime.now()` without a timezone, or a mixture of older and newer APIs. Do not remove timezone information from the aware value just to make the exception disappear. That would leave two wall-clock readings whose relationship to real instants may be unknown. First inspect the values and their tzinfo, then identify the source contract for the naive timestamp.
from datetime import datetime, timezone
deadline = datetime(2026, 10, 13, 10, 0)
event = datetime(2026, 10, 13, 9, 0, tzinfo=timezone.utc)
print(event < deadline)TypeError: can't compare offset-naive and offset-aware datetimesInspect tzinfo and the source's meaning
Print each datetime with isoformat() and print tzinfo. A naive datetime has no usable UTC offset; in our example its tzinfo is None. The aware event carries `+00:00`, which tells Python it is in UTC. A naive value is not automatically UTC. It might mean a local civil time, a timestamp that lost its offset during parsing, or a genuinely UTC timestamp stored without a label.
The right fix depends on that provenance. If an API documents that its offset-free values are UTC, you can explicitly attach timezone.utc. If the value is a local clock time in a named region, use that region's timezone with a deliberate daylight-saving policy. Do not assume the machine's current timezone represents the original sender's timezone. Ordering two aware datetimes works because Python can account for their offsets.
from datetime import datetime, timezone
deadline = datetime(2026, 10, 13, 10, 0)
event = datetime(2026, 10, 13, 9, 0, tzinfo=timezone.utc)
print(deadline.isoformat(), deadline.tzinfo)
print(event.isoformat(), event.tzinfo)2026-10-13T10:00:00 None
2026-10-13T09:00:00+00:00 UTCAttach a timezone only when the source is known
Suppose the data contract states that the offset-free deadline is in UTC. Use `deadline.replace(tzinfo=timezone.utc)` to label that existing wall-clock value as UTC, then compare it with the aware event. This operation does not convert the hour; it declares what the original hour meant. The event at 09:00 UTC is before the 10:00 UTC deadline, so the comparison prints True.
For an already aware value in another offset, use `astimezone(timezone.utc)` to convert the instant to UTC. It may change the displayed hour while preserving the instant. `replace(tzinfo=timezone.utc)` on a time that actually meant New York would mislabel the instant. If the source timezone is unknown, get that information from the data contract or user rather than guessing. Where possible, save and exchange timezone-aware ISO 8601 timestamps to avoid reintroducing ambiguity.
from datetime import datetime, timezone
deadline_utc_naive = datetime(2026, 10, 13, 10, 0)
deadline_utc = deadline_utc_naive.replace(tzinfo=timezone.utc)
event_utc = datetime(2026, 10, 13, 9, 0, tzinfo=timezone.utc)
print(event_utc < deadline_utc)Truefrom datetime import datetime, timezone, timedelta
east = timezone(timedelta(hours=2))
event = datetime(2026, 10, 13, 11, 0, tzinfo=east)
print(event.astimezone(timezone.utc).isoformat())2026-10-13T09:00:00+00:00Reject ambiguity and test equivalent instants
A robust comparison helper can require aware inputs rather than silently interpreting a naive datetime. Check that `utcoffset()` is not None for both arguments, normalize each with astimezone(timezone.utc), then compare. Test two representations of the same instant, one before the cutoff, and a naive input that should be rejected. Equal instants should not be reported as earlier even when their printed local hours differ.
This example uses fixed offsets to keep the result deterministic in the browser console. Real geographical zones can change their UTC offset with daylight-saving rules; the standard library's zoneinfo module handles named IANA zones when the timezone data is available. Ambiguous and nonexistent local times need separate product rules. The main repair here is to make both values aware using trustworthy source information before ordering them.
from datetime import datetime, timezone, timedelta
cutoff = datetime(2026, 10, 13, 9, 0, tzinfo=timezone.utc)
east = timezone(timedelta(hours=2))
same_in_east = datetime(2026, 10, 13, 11, 0, tzinfo=east)
print(same_in_east == cutoff)
print(same_in_east < cutoff)True
False- A naive datetime does not identify an offset or an unambiguous instant.
- Attach tzinfo only when the source timezone is known; replace does not convert time.
- Convert already-aware instants with astimezone() and test equivalent instants.
Practice the fix
Write happened_before(event, cutoff) for two timezone-aware datetime objects. Raise ValueError('timezone required') if either is naive. Otherwise return whether event occurred before cutoff, even when the two inputs use different fixed offsets. Test an earlier event, the same instant in two offsets, and a naive event.
Need a hint?
A datetime is naive when its utcoffset() is None. Once both values are aware, compare event.astimezone(timezone.utc) with cutoff.astimezone(timezone.utc).
Python console
Ready to runEdit the code and run it in your browser. Examples above can be loaded with Try this example.
Does your code use input()? Add one value per line
Your output appears here.
Write your solution, then select Check practice.
Key takeaways
- Comparing a naive and an aware datetime with ordering raises TypeError.
- A naive wall-clock value needs a trustworthy source timezone before it identifies an instant.
- replace(tzinfo=...) labels a value; astimezone(...) converts an aware instant.
- Verify equivalent instants across offsets and reject ambiguous inputs.