Python

Python Datetime Timezone Conversion Errors

Most Python datetime timezone bugs come down to mixing "naive" and "aware" datetime objects — Python allows both to exist side by side without warning, but comparing or converting between them incorrectly produces subtly wrong results, or an outright error, depending on the operation.

The Problem

Datetime comparisons, conversions, or calculations produce wrong results or raise errors:

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

Or, more subtly, no error at all, but times displayed to users are off by several hours, without any obvious cause in the code.

Why It Happens

Python's datetime objects can exist in two distinct states, and code that doesn't consistently track which state it's working with runs into problems:

  • A "naive" datetime has no timezone information attached at all — it's just a date and time with no context about which timezone it represents, and Python has no way to know whether it means UTC, local time, or something else
  • An "aware" datetime has explicit timezone information attached, and Python can correctly compare and convert it relative to other aware datetimes
  • Mixing the two — trying to subtract or compare a naive datetime against an aware one — raises a TypeError, since Python can't determine what the naive datetime's timezone context actually is
  • Even when both are aware, using outdated timezone libraries or incorrect API usage can produce wrong conversions, particularly around daylight saving time transitions where the offset for a given timezone changes partway through the year

The Fix

Use Python's built-in zoneinfo module (available from Python 3.9 onward) rather than manually managing UTC offsets, since it correctly handles daylight saving time transitions and historical timezone rule changes that manual offset math gets wrong:

from datetime import datetime
from zoneinfo import ZoneInfo

# Create an aware datetime explicitly
dt = datetime(2026, 7, 28, 14, 30, tzinfo=ZoneInfo("America/Sao_Paulo"))
print(dt)

To convert an aware datetime from one timezone to another, use astimezone(), which correctly recalculates the wall-clock time for the target timezone:

utc_dt = dt.astimezone(ZoneInfo("UTC"))
print(utc_dt)

If you're working with a naive datetime that you know represents a specific timezone (common when reading from a database or API that doesn't include timezone info explicitly), attach the correct timezone using replace() rather than assuming Python will infer it:

naive_dt = datetime(2026, 7, 28, 14, 30)
aware_dt = naive_dt.replace(tzinfo=ZoneInfo("America/Sao_Paulo"))

Be careful not to confuse replace(tzinfo=...) with astimezone()replace attaches a timezone to a naive datetime without changing the actual time values (treating the existing numbers as if they were already in that timezone), while astimezone converts an already-aware datetime, actually recalculating the time values for the new timezone. Using the wrong one for your situation produces a datetime with correct-looking timezone info but wrong underlying time, or vice versa.

For getting the current time, always use timezone-aware functions rather than naive ones, which are being deprecated specifically because of how easily they lead to this category of bug:

# Avoid: naive, ambiguous about what timezone this represents
now = datetime.now()

# Prefer: explicit, unambiguous
now_utc = datetime.now(ZoneInfo("UTC"))

If you're on a Python version older than 3.9 without zoneinfo, or need to support timezone databases across a wider range of environments consistently, the well-established third-party pytz library serves the same purpose, though its API has a few historical quirks (like requiring localize() rather than direct construction with tzinfo) worth reading the documentation for specifically before using it.

Still Not Working?

If conversions look correct most of the time but are occasionally off by exactly one hour, suspect a daylight saving time transition boundary — a datetime very close to when clocks change can behave unexpectedly if the conversion logic doesn't correctly account for the transition. Using zoneinfo (or a well-maintained library like pytz) rather than manual fixed-offset math is the correct fix here, since only a proper timezone database has the specific transition rules for each region baked in, rather than assuming a constant offset year-round.