Introduction
Python raises TypeError: can't subtract offset-naive and offset-aware datetimes when you try to subtract a timezone-aware datetime from a naive one (or vice versa). A naive datetime has no timezone info (tzinfo=None), while an aware datetime includes timezone data. The fix is to make both datetimes either naive or aware before performing arithmetic. Use datetime.now(timezone.utc) for aware datetimes, or dt.replace(tzinfo=None) to strip timezone info.
The Error
1from datetime import datetime, timezone
2
3naive = datetime(2024, 6, 15, 12, 0, 0) # No timezone
4aware = datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc) # UTC
5
6diff = aware - naive
7# TypeError: can't subtract offset-naive and offset-aware datetimes
Naive vs Aware Datetimes
1from datetime import datetime, timezone
2
3# Naive — no timezone information
4naive = datetime.now()
5print(naive.tzinfo) # None
6
7# Aware — has timezone information
8aware = datetime.now(timezone.utc)
9print(aware.tzinfo) # UTC
10
11# Check if a datetime is naive or aware
12def is_aware(dt):
13 return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
14
15print(is_aware(naive)) # False
16print(is_aware(aware)) # True
Fix 1: Make Both Aware (Recommended)
1from datetime import datetime, timezone
2
3# Make naive datetime aware by adding UTC timezone
4naive = datetime(2024, 6, 15, 12, 0, 0)
5aware = datetime.now(timezone.utc)
6
7# Option A: replace tzinfo (assumes the naive time is UTC)
8naive_as_utc = naive.replace(tzinfo=timezone.utc)
9diff = aware - naive_as_utc
10print(diff) # timedelta result
11
12# Option B: use zoneinfo (Python 3.9+)
13from zoneinfo import ZoneInfo
14
15naive_as_eastern = naive.replace(tzinfo=ZoneInfo("America/New_York"))
16diff = aware - naive_as_eastern
17print(diff)
Fix 2: Make Both Naive
1from datetime import datetime, timezone
2
3aware = datetime.now(timezone.utc)
4naive = datetime(2024, 6, 15, 12, 0, 0)
5
6# Strip timezone from the aware datetime
7aware_as_naive = aware.replace(tzinfo=None)
8diff = aware_as_naive - naive
9print(diff) # Works — both are naive now
This loses timezone information, so only use it when you know both datetimes represent the same timezone.
Fix 3: Using pytz (Pre-3.9)
1import pytz
2from datetime import datetime
3
4# Localize a naive datetime to a timezone
5eastern = pytz.timezone("America/New_York")
6naive = datetime(2024, 6, 15, 12, 0, 0)
7
8# CORRECT way to make aware with pytz
9aware_eastern = eastern.localize(naive)
10
11# Now subtract from another aware datetime
12utc_now = datetime.now(pytz.utc)
13diff = utc_now - aware_eastern
14print(diff)
Fix 4: Using zoneinfo (Python 3.9+)
1from datetime import datetime, timezone
2from zoneinfo import ZoneInfo
3
4# Create aware datetimes directly
5utc_time = datetime(2024, 6, 15, 16, 0, 0, tzinfo=timezone.utc)
6tokyo_time = datetime(2024, 6, 16, 1, 0, 0, tzinfo=ZoneInfo("Asia/Tokyo"))
7
8# Subtraction works — both are aware
9diff = tokyo_time - utc_time
10print(diff) # 0:00:00 (same instant in time)
Working with Database Datetimes
Databases often return naive datetimes even when the data was stored as UTC:
1from datetime import datetime, timezone
2
3# Simulating a database result (naive, but actually UTC)
4db_created_at = datetime(2024, 6, 15, 12, 0, 0) # From database
5
6# Application code uses aware datetimes
7now = datetime.now(timezone.utc)
8
9# Option 1: Make the DB datetime aware
10created_utc = db_created_at.replace(tzinfo=timezone.utc)
11age = now - created_utc
12
13# Option 2: Configure your DB driver to return aware datetimes
14# SQLAlchemy:
15# Column(DateTime(timezone=True))
16# Django:
17# USE_TZ = True in settings.py
18# psycopg2:
19# Automatically returns aware datetimes for timestamptz columns
Working with pandas Timestamps
1import pandas as pd
2from datetime import datetime, timezone
3
4# pandas Timestamps can be naive or aware
5naive_ts = pd.Timestamp("2024-06-15 12:00:00")
6aware_ts = pd.Timestamp("2024-06-15 12:00:00", tz="UTC")
7
8# Localize naive to aware
9aware_ts = naive_ts.tz_localize("UTC")
10
11# Convert between timezones
12eastern_ts = aware_ts.tz_convert("America/New_York")
13
14# For entire Series/DataFrame columns
15df = pd.DataFrame({"date": pd.to_datetime(["2024-01-01", "2024-06-15"])})
16df["date_utc"] = df["date"].dt.tz_localize("UTC")
17df["date_eastern"] = df["date_utc"].dt.tz_convert("America/New_York")
Best Practices
1from datetime import datetime, timezone
2
3# ALWAYS create aware datetimes
4now = datetime.now(timezone.utc) # Preferred over datetime.utcnow()
5# datetime.utcnow() returns a NAIVE datetime in UTC — confusing and deprecated in 3.12
6
7# When parsing strings, always specify timezone
8from datetime import datetime
9from zoneinfo import ZoneInfo
10
11parsed = datetime.fromisoformat("2024-06-15T12:00:00+00:00") # Aware
12parsed_naive = datetime.fromisoformat("2024-06-15T12:00:00") # Naive — avoid
13
14# Store as UTC, display in local timezone
15utc_time = datetime.now(timezone.utc)
16local_time = utc_time.astimezone(ZoneInfo("America/Los_Angeles"))
17print(local_time.strftime("%Y-%m-%d %I:%M %p %Z"))
Common Pitfalls
Using datetime.utcnow() and assuming it is timezone-aware: datetime.utcnow() returns a naive datetime that happens to be in UTC. It has tzinfo=None, so subtracting it from an aware datetime still raises TypeError. Use datetime.now(timezone.utc) instead. utcnow() is deprecated in Python 3.12.
Using replace(tzinfo=) when you mean localize(): replace(tzinfo=tz) blindly attaches a timezone without adjusting the time. For pytz timezones with historical offset changes, this can produce incorrect times. Use tz.localize(naive_dt) with pytz, or replace() only with fixed-offset zones like timezone.utc.
Stripping timezone to avoid the error: aware.replace(tzinfo=None) silently drops timezone information. If the two datetimes were in different timezones, the subtraction result is wrong. Convert both to the same timezone first, then subtract.
Mixing pytz and zoneinfo: pytz.timezone("US/Eastern") and ZoneInfo("US/Eastern") produce different tzinfo objects. Comparing or subtracting datetimes with mixed timezone backends can give unexpected results. Use one library consistently.
Forgetting DST transitions: Subtracting two aware datetimes across a DST boundary correctly accounts for the clock change. But if you strip timezones and subtract naive datetimes, the result is off by one hour during DST transitions. Always keep timezone information for accurate duration calculations.
Summary
The error occurs when subtracting a naive datetime (no timezone) from an aware one (has timezone)
Fix by making both datetimes aware (replace(tzinfo=timezone.utc) or tz_localize) or both naive
Prefer aware datetimes throughout your application — use datetime.now(timezone.utc)
Use zoneinfo.ZoneInfo (Python 3.9+) or pytz for non-UTC timezones
datetime.utcnow() is deprecated in Python 3.12 — use datetime.now(timezone.utc) instead
Store timestamps as UTC in databases and convert to local time only for display