Code review example: datetime.now() is naive: comparing it to an aware datetime raises, and it's local time anyway in Python

Retired from the DiffDojo practice pool in October 2026; the review below is the canonical answer. An AI wrote this pull request for the task underneath. Read the diff and decide what you would say before you scroll to the review.

Python · FastAPI · Data correctness · intermediate

The task

Check whether a token has expired

  1. is_expired(token) returns True when the token's expiry has passed.
  2. token.expires_at is a timezone-aware UTC datetime (from the database).
  3. The comparison must be correct regardless of the server's local timezone.

Adds the expiry check: compares the token's expiry against the current time and returns whether it has passed. Straightforward datetime comparison.

Servers run in various regions; the DB stores expires_at as timezone-aware UTC.

The diff

app/auth/expiry.py+5
1+ from datetime import datetime
2+
3+
4+ def is_expired(token) -> bool:
5+ return token.expires_at < datetime.now()

The review

Issue 1: datetime.now() is naive: comparing it to an aware datetime raises, and it's local time anyway

app/auth/expiry.py, lines 1, 5

  1. datetime.now() is naive and local. It carries no timezone and reflects the server's wall clock. token.expires_at is timezone-aware UTC.
  2. Comparing the two raises. Python refuses to order a naive and an aware datetime: TypeError: can't compare offset-naive and offset-aware datetimes. The endpoint 500s the moment it checks a token.
  3. The naive path would be a trap too. If expires_at were stored naive-but-meaning-UTC (a common mistake), comparing it against local datetime.now() would be silently wrong by the server's UTC offset, Tokyo and New York would disagree by hours. Naive datetimes are only safe when every value in the comparison is consistently the same zone; mixing a UTC-meaning value with a local-meaning now() is the danger.

The fix: datetime.now(timezone.utc), an aware UTC value that compares correctly against the aware expires_at everywhere.

The fix

from datetime import datetime, timezone


def is_expired(token) -> bool:
    return token.expires_at < datetime.now(timezone.utc)

Why it matters

datetime.now() / utcnow() with no tzinfo → you have a naive datetime. The moment it meets an aware one, Python raises; and even alone, now() is local time masquerading as truth. Use datetime.now(timezone.utc).

Real-world stakes: every token check 500s in production the first time it runs against a real timezone-aware expiry, or, in the naive-DB variant, tokens expire hours early or late depending on where the server sits. Naive/aware datetime mixing is Python's most shipped date bug, and assistants default to bare datetime.now() because it's the shortest thing that looks like "current time".

Read more: python offset-naive offset-aware datetime compare, datetime.now timezone.utc aware, utcnow deprecated timezone aware

The verdict

Request changes. datetime.now() is naive: comparing it to an aware datetime raises, and it's local time anyway

Try a live one

Python PRs you have not seen the answer to.

Leave inline comments and a verdict, then see what you caught. Free, no signup.

Review a Python PR →

How to review Python code · All code review examples