TypeError: can't compare offset-naive and offset-aware datetimes は、タイムゾーンの情報が無い日時(naive)と、ある日時(aware)を、<・> で比べたときのエラーです。 引き算では can't subtract offset-naive and offset-aware datetimes になります。

種類 例 tzinfo
naive(タイムゾーンなし) datetime.now()、datetime(2026, 10, 11, 9, 0) None
aware(タイムゾーンあり) datetime.now(timezone.utc)、PostgreSQL の timestamptz UTC・Asia/Tokyo など

直し方は、どちらかにそろえることです。システム全体を aware(タイムゾーンあり)にそろえるのが安全です。

from datetime import datetime, timezone
now = datetime.now(timezone.utc)          # datetime.now() ではなく、タイムゾーン付きで

確認環境:Python 3.10・3.11・3.12、psycopg 3.3.6(PostgreSQL 16)、PyMySQL(MySQL 8.0)、pandas 3.0。OS のタイムゾーンは日本時間(確認日 2026年10月11日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「offset-naive and offset-awareの直し方|Python」

再現:比べ方によって、エラーになる・ならない

naive = datetime(2026, 10, 11, 9, 0)
aware = datetime(2026, 10, 11, 9, 0, tzinfo=ZoneInfo('Asia/Tokyo'))
操作 結果(確認環境)
naive < aware TypeError: can't compare offset-naive and offset-aware datetimes
aware - naive TypeError: can't subtract offset-naive and offset-aware datetimes
sorted([aware, naive])・max(naive, aware) TypeError(中で比べるため)
naive == aware False(エラーにならない)

== だけはエラーにならず、常に False になりました(公式ドキュメントでも、naive と aware は決して等しくならないとされています)。「同じ時刻のはずなのに一致しない」という不具合は、これが原因のことがあります。

どこから naive・aware が来るか

作り方 結果
datetime.now() naive
datetime.now(timezone.utc) aware(UTC)
datetime.utcnow() naive(UTC の時刻だが、タイムゾーンは付かない)
datetime.fromisoformat('2026-10-11T00:00:00Z') aware(3.11 以降。3.10 では Invalid isoformat string)
datetime.fromisoformat('2026-10-11T00:00:00') naive
strptime(..., '%Y-%m-%d %H:%M:%S%z') aware
strptime(..., '%Y-%m-%d %H:%M:%S') naive

datetime.utcnow() は、UTC の時刻を返しますが、タイムゾーンは付きません。 Python 3.12 では DeprecationWarning: datetime.datetime.utcnow() is deprecated の警告が出ました。公式ドキュメントでも、代わりに datetime.now(timezone.utc) を使うよう書かれています。

DB のドライバーで、返ってくる日時が違う

DB・型 Python に返る値(確認環境)
PostgreSQL の timestamptz(psycopg) aware(接続の TimeZone の時刻。Etc/UTC なら UTC、Asia/Tokyo なら +09:00)
PostgreSQL の timestamp naive
MySQL の DATETIME・TIMESTAMP(PyMySQL) naive(どちらも)
row_time = conn.execute("SELECT now()").fetchone()[0]     # PostgreSQL:aware
row_time < datetime.now()                                  # naive と比べる
# TypeError: can't compare offset-naive and offset-aware datetimes

PostgreSQL の timestamptz の値と datetime.now() を比べると、このエラーになりました。 MySQL から PostgreSQL に移したとき、比べる側のコードはそのままで、DB の値だけ aware に変わって出る、という形です。

MySQL では、エラーにならずに9時間ずれた

MySQL の NOW()(サーバーは UTC)  : 2026-10-10 17:45:28   ← naive
Python の datetime.now()(日本時間): 2026-10-11 02:45:28   ← naive
差:9.0 時間

どちらも naive なのでエラーにはなりませんが、DB のサーバーは UTC、Python は日本時間で、同じ瞬間が9時間ずれた値として比べられました。 エラーになる PostgreSQL のほうが、まだ気づきやすいと言えます。日時が9時間ずれる問題の全体は、関連記事の「日時が9時間ずれる原因と直し方|Java・JS・MySQL・PG・Docker」で扱っています。

直し方:replace と astimezone の違い

naive を aware にするとき、replace と astimezone で結果が違います。

naive = datetime(2026, 10, 11, 9, 0)            # 9時(どこの9時かは不明)
書き方 結果(確認環境) 意味
naive.replace(tzinfo=timezone.utc) 2026-10-11T09:00:00+00:00 時刻はそのまま、UTC の印を付ける
naive.replace(tzinfo=ZoneInfo('Asia/Tokyo')) 2026-10-11T09:00:00+09:00 時刻はそのまま、日本時間の印を付ける
naive.astimezone(timezone.utc) 2026-10-11T00:00:00+00:00 naive を「OS の時刻(日本時間)」とみなして、UTC に変換

astimezone は、naive の値を OS のタイムゾーンの時刻とみなします(公式ドキュメント)。同じコードでも、日本時間のパソコンと UTC のサーバーで結果が変わります。値がどのタイムゾーンの時刻か分かっているなら、replace(tzinfo=...) で印を付けます。

状況 書き方
naive の値が UTC の時刻(utcnow()・UTC のサーバーの DB) replace(tzinfo=timezone.utc)
naive の値が日本時間の時刻 replace(tzinfo=ZoneInfo('Asia/Tokyo'))
aware を別のタイムゾーンで表したい astimezone(ZoneInfo('Asia/Tokyo'))
aware から印を外したい(naive しか扱えない処理に渡す) replace(tzinfo=None)(時刻はそのまま)

PostgreSQL の接続の TimeZone

conn.execute("SET TimeZone = 'Asia/Tokyo'")
conn.execute("SELECT now()").fetchone()[0]     # 2026-10-11T02:45:28+09:00

timestamptz の値は、接続の TimeZone の時刻で返ってきました(同じ瞬間を、+09:00 で表した値)。どちらで返っても aware なので、比べる・引き算する分には問題ありません。

pandas の場合

pd.Timestamp('2026-10-11 09:00') < pd.Timestamp('2026-10-11 09:00', tz='Asia/Tokyo')
TypeError: Cannot compare tz-naive and tz-aware timestamps

df[df['at'] > datetime(2026, 10, 11, 12, 0)]           # at はタイムゾーン付きの列
TypeError: Invalid comparison between dtype=datetime64[us, Asia/Tokyo] and datetime

pandas では、メッセージの文が違います。 比べる値にもタイムゾーンを付けます。

df[df['at'] > pd.Timestamp('2026-10-11 12:00', tz='Asia/Tokyo')]
pd.Timestamp('2026-10-11 09:00').tz_localize('Asia/Tokyo').tz_convert('UTC')   # 2026-10-11 00:00:00+00:00

pandas の tz_localize は replace(tzinfo=...)、tz_convert は astimezone に当たります。

確認の手順(チェックリスト)

順番 確認すること 方法
1 どちらが naive か print(x.tzinfo, y.tzinfo)(None が naive)
2 naive はどこから来たか datetime.now()・utcnow()・MySQL・タイムゾーンの無い文字列
3 その naive は、どこの時刻か UTC か日本時間かを決める
4 印を付ける replace(tzinfo=...)(astimezone は OS の時刻とみなす)
5 == で比べていないか naive と aware は常に False
6 今後 datetime.now(timezone.utc) で統一

よくある質問

Q. 全部 naive にそろえてもよいですか。 A. エラーは消えますが、上の MySQL の例のように、どこの時刻かが分からない値どうしを比べて、黙ってずれる危険があります。保存・計算は UTC の aware、画面に出すときだけ日本時間に変換、とそろえるのが安全です。

まとめ

  • タイムゾーンなし(naive)とあり(aware)を、比べた・引いたエラー
  • == だけはエラーにならず、常に False
  • PostgreSQL の timestamptz は aware、MySQL の DATETIME は naive で返る
  • replace は印を付けるだけ、astimezone は naive を OS の時刻とみなして変換
  • utcnow() は naive で、3.12 から非推奨。datetime.now(timezone.utc) に
  • pandas は Cannot compare tz-naive and tz-aware。tz_localize・tz_convert

参考・出典

確認日はいずれも 2026年10月11日です。

  • Python ドキュメント「datetime — Basic date and time types」:https://docs.python.org/3/library/datetime.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「offset-naive and offset-awareの直し方|Python」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?