できない.dev

Python で datetime の naive と aware を比較できず TypeError になる

can't compare offset-naive and offset-aware datetimes は tzinfo の有無が違う datetime を比較・減算したときに起きる。
両方を aware(zoneinfo / timezone.utc)に揃えると解決する。

#python#datetime#timezone#naive#aware

公開:

要約

TypeError: can't compare offset-naive and offset-aware datetimes は、片方に tzinfo が付いて aware、もう片方が naive のまま比較・減算した場合に発生する。
両方を aware に揃えるのが基本方針で、Python 3.9 以降は標準モジュールの zoneinfo を使うのが推奨。

よくある原因

  1. datetime.now()datetime.now(timezone.utc) の混在: 引数なしの now() は naive、tzinfo を渡すと aware で結果の型が異なる。
  2. 外部ライブラリは aware: SQLAlchemy / pydantic / Django ORM などは多くの場合 aware で値を返すため、自前の naive と比較すると落ちる。
  3. ISO 文字列のタイムゾーン欠落: datetime.fromisoformat("2025-01-01T12:00:00") はタイムゾーン情報なしで naive、末尾に +09:00 が付くと aware。
  4. pytzzoneinfo の混在: 古い pytz 由来の tzinfo は localize 経由でないと正しく扱えず、zoneinfo と挙動が違う場面がある。

解決策

1. 両方を aware にする

from datetime import datetime, timezone
from zoneinfo import ZoneInfo
 
now = datetime.now(ZoneInfo("Asia/Tokyo"))
later = datetime.now(timezone.utc)
 
# どちらも aware なので比較・減算が可能
print(later > now)

datetime 公式ドキュメント のとおり、tzinfo を持つかどうかで naive / aware が決まる。

2. naive を aware に昇格

from datetime import datetime
from zoneinfo import ZoneInfo
 
naive = datetime(2025, 1, 1, 12, 0)
aware = naive.replace(tzinfo=ZoneInfo("Asia/Tokyo"))

replace(tzinfo=...) は時刻の数字をそのまま保ったまま「これは JST だ」と注釈を付ける操作。
UTC に変換したいなら astimezone(timezone.utc) を続けて呼ぶ。

3. aware を naive に揃える(推奨しない)

from datetime import datetime, timezone
from zoneinfo import ZoneInfo
 
aware = datetime.now(timezone.utc)
naive = aware.astimezone(ZoneInfo("Asia/Tokyo")).replace(tzinfo=None)

DB の制約で naive しか保存できない場合の最終手段。
意図しないタイムゾーン差を生むので、可能ならスキーマ側を aware に直す方が安全。

4. プロジェクト全体のルール統一

実用上は「内部表現は UTC aware、ユーザー表示で ZoneInfo 変換」が事故が少ない。ZoneInfo 自体は zoneinfo モジュール公式 のとおり標準ライブラリに含まれており、pytz は新規採用しなくてよい。

この記事は役立ちましたか?