こんにちは、おうどんです🍜
朝9時。 うどん屋さんの電話が鳴る。
支店「9時に出前お願いします!」
店長(おうどん)「了解、9時ね」
支店「はい、9時です!」
店長「……どこの9時?」
(場面は説明用の架空のものです)
支店は海外にありました。 向こうの9時は、こっちの夜。 うどんは、誰もいない店に届きました。。。
これ、Python の datetime(日時を扱う標準ライブラリ)でも毎日のように起きています。 しかも Python は、うどん屋さんより慎重です。 「どこの9時か分からない時刻」と「どこの9時か分かる時刻」を比べようとすると、こう言って止まります。
TypeError: can't compare offset-naive and offset-aware datetimes
Python「比べられません。片方、どこの9時か書いてないので」
……出前は届けちゃう店長より、ずっと偉い。
今回は、このエラーの原因と直し方から始めて、utcnow() の非推奨、Windows で ZoneInfo が見つからない件、夏時間の落とし穴まで掘っていきます。
結論:まずはこれだけ
先に答えを置いておきます。時刻には「どこの時刻か」の札を付けて、札付きどうしで比べる、です。
- 今の時刻は
datetime.now(timezone.utc)で取る(datetime.utcnow()は使わない) - 札のない時刻は、どこの時刻かを決めてから
replace(tzinfo=...)で札を付ける - 別の地域の時刻に直すときは
astimezone(...)を使う
全部の時刻を文字列にして比べれば、エラーは出ないのでは?
エラーは出ません。 そのかわり「09:00+09:00」と「00:30Z」を文字の順で並べて、本当の順番とは違う結果を、何食わぬ顔で返してきます。 エラーが出ないほうが、こわいんです。
じゃあ全部の時刻から札をはがせば、比べられる?
比べられます。 支店の9時と本店の9時が、同じ9時になります。 出前は、また誰もいない店に届きます。
naive と aware ってなに?
Python の日時には、札なしと札付きの2種類があります。札なし(naive)は「どこの9時か」を知らない時刻、札付き(aware)は知っている時刻です。
datetime のドキュメントでは、こう定義されています。
- aware:ほかの aware な時刻との位置関係が決められる。解釈の余地のない、特定の瞬間を表す
- naive:ほかの時刻と位置関係を決めるだけの情報を持っていない
datetimeのdが aware なのは、d.tzinfoがNoneでなく、かつd.tzinfo.utcoffset(d)がNoneを返さないとき
tzinfo(タイムゾーンの情報を持つ属性)が札、utcoffset(UTC からのずれ)が札に書いてある数字、と思ってください。 UTC は協定世界時、世界の時計の基準です。日本時間は UTC より9時間進んでいて、+09:00 と書きます。
札なしと札付きを混ぜると、どうなるか。
t1_mix.py:
from datetime import datetime, timezone, timedelta
JST = timezone(timedelta(hours=9))
naive = datetime(2026, 10, 1, 9, 0) # タイムゾーンなし
aware = datetime(2026, 10, 1, 9, 0, tzinfo=JST) # +09:00 付き
print("naive:", naive.isoformat(), naive.tzinfo)
print("aware:", aware.isoformat(), aware.tzinfo)
print("naive == aware:", naive == aware)
try:
print(naive < aware)
except TypeError as e:
print("比較:", e)
try:
print(aware - naive)
except TypeError as e:
print("引き算:", e)
python -X utf8 t1_mix.py
naive: 2026-10-01T09:00:00 None
aware: 2026-10-01T09:00:00+09:00 UTC+09:00
naive == aware: False
比較: can't compare offset-naive and offset-aware datetimes
引き算: can't subtract offset-naive and offset-aware datetimes
(今回の検証環境の Windows 10・Python 3.13.1 で実行した結果です)
ドキュメントのとおりの動きです。
- 大小の比較(
<や>)は TypeError - 引き算も TypeError
==だけはエラーにならず、必ず False(Python 3.3 から、==では TypeError を出さなくなった)
札なしくん「9時です!」
札付きさん「9時です。+09:00 の」
Python「同じ9時ですか?」
札なしくん「たぶん!」
Python「……違うことにしておきます」
……たぶん、では同じにしてもらえない。
同じ9時って書いてあるのに False は、さすがに冷たくない?
札なしくんの9時は、東京の9時かもしれないし、ロンドンの9時かもしれません。 「かもしれない」を同じ扱いにすると、あとで出前が迷子になります。 Python は、疑わしきは別人、の方針なんです。
いちばん多い原因:オフセットのある文字列とない文字列が混ざる
実務でこのエラーに出会う一番の入口は、いろいろな場所から集めた時刻の文字列を、そのまま読み込んで並べたときです。
API(プログラム同士がやりとりする窓口)は +09:00 付き、クラウドのログは末尾に Z(UTC の意味)、古い CSV はオフセットなし。 よくある組み合わせを、fromisoformat()(ISO 8601 という日時の書式の文字列を読む関数)で読んでみます。
t2_sort.py:
from datetime import datetime
# ログから集めた時刻(説明用のデータ)
rows = [
"2026-10-01T09:00:00+09:00", # API から
"2026-10-01T00:30:00Z", # クラウドのログから
"2026-10-01 08:45:00", # 古い CSV から(オフセットなし)
]
times = [datetime.fromisoformat(s) for s in rows]
for s, t in zip(rows, times):
print(f"{s:<28} -> tzinfo={t.tzinfo}")
print(sorted(times))
python -X utf8 t2_sort.py
2026-10-01T09:00:00+09:00 -> tzinfo=UTC+09:00
2026-10-01T00:30:00Z -> tzinfo=UTC
2026-10-01 08:45:00 -> tzinfo=None
Traceback (most recent call last):
File "t2_sort.py", line 13, in <module>
print(sorted(times))
~~~~~~^^^^^^^
TypeError: can't compare offset-naive and offset-aware datetimes
(今回の検証環境の Windows 10・Python 3.13.1 で実行した結果です。標準出力と標準エラーを続けて載せ、トレースバックのファイルのパスは短くしています)
読み込みまでは、すんなり通ります。 エラーになるのは、sorted() の中で大小を比べた瞬間です。
sorted「並べます! 1番目と2番目を比べて……3番目、札がないですね。無理です」
店長「読み込みのときに言ってよ」
fromisoformat「書いてあるとおりに読んだだけです」
……誰も悪くないのに、止まる。
なので、エラーの行(sorted() や max()、<)を直すのではなく、時刻を読み込んだ場所を探すのが近道です。 print(t.tzinfo) で、どれが None(札なし)かを見ましょう。
CSV の作った人に、札を付けてもらえばいいのでは?
それが一番です。 ただ、その CSV は5年前に作られて、作った人はもう別の部署にいます。たぶん。 だから、読み込む側で札を付けます。次の章です。
直し方:札を付ける replace、時差を直す astimezone
札の付け方は2通りあって、役目がまったく違います。
replace(tzinfo=tz):時刻の数字はそのままで、札だけ付け替えるastimezone(tz):同じ瞬間のまま、tzの地域の時刻に換算する
ドキュメントでも、時刻を変えずにタイムゾーンを付けたいだけなら replace(tzinfo=tz)、astimezone() は「UTC で同じ時刻になるように」日時を調整する、と説明されています。
t5_replace.py:
from datetime import datetime, timezone, timedelta
JST = timezone(timedelta(hours=9))
t = datetime(2026, 10, 1, 9, 0, tzinfo=JST) # 日本時間の朝9時
print("元の時刻 :", t)
print("astimezone(UTC) :", t.astimezone(timezone.utc))
print("replace(UTC) :", t.replace(tzinfo=timezone.utc))
print("同じ瞬間? astimezone:", t == t.astimezone(timezone.utc))
print("同じ瞬間? replace :", t == t.replace(tzinfo=timezone.utc))
元の時刻 : 2026-10-01 09:00:00+09:00
astimezone(UTC) : 2026-10-01 00:00:00+00:00
replace(UTC) : 2026-10-01 09:00:00+00:00
同じ瞬間? astimezone: True
同じ瞬間? replace : False
(今回の検証環境の Windows 10・Python 3.13.1 で実行した結果です)
astimezone は、日本の9時を「UTC の0時」に換算しました。同じ瞬間です。 replace は、数字の9時はそのままで札を「UTC」に貼り替えました。9時間ずれた、別の瞬間になっています。
使い分けはこうです。
- 札なしの時刻に「これは日本時間です」と札を付ける →
replace - 札付きの時刻を、別の地域の時刻に直す →
astimezone
店長「東京の9時の伝票、ロンドン支店に回して」
見習いさん「はい! 伝票の『東京』を消して『ロンドン』と書きました!」
店長「……9時のまま?」
見習いさん「9時のままです!」
……それは換算ではなく、改ざんです。
全部 astimezone にしておけば、安全なのでは?
札なしの時刻に astimezone を使うと、ドキュメントのとおり「この PC のタイムゾーンの時刻」とみなされます(Python 3.6 から)。 CSV の時刻が本当は UTC だったら、9時間ずれます。 札なしの時刻には、まず自分で replace で札を付ける。これが確実です。
🔰 ここまで読めば今日から困らない
- naive(札なし)と aware(札付き)は、大小の比較も引き算もできない。
==は必ず False - エラーの行ではなく、時刻を読み込んだ場所で
tzinfoがNoneのものを探す - 札なしの時刻には
replace(tzinfo=...)で札を付け、別の地域に直すときはastimezone(...) - 今の時刻は
datetime.now(timezone.utc)で取る
4つ。出前の電話で「どこの9時?」と聞き返すのと、ほぼ同じ手間。
聞き返すのは一瞬です。 誰もいない店にうどんを届けて、のびたうどんを持って帰るよりは、ずっと早い。
聞き返したら「えっ、9時は9時ですよ」って言われたら?
それが札なしくんです。 そのときは、店長が「東京の9時ね」と決めて伝票に書く。replace の出番です。
ここから先は中級者向け。読み飛ばしてもOKです。
中級:utcnow() は UTC なのに、札が付いていない
札なしの時刻を量産している犯人が、意外なところにいます。datetime.utcnow() は UTC の今の時刻を返すのに、札(tzinfo)を付けません。
ドキュメントには、こう書かれています。
- naive な datetime は、多くのメソッドでローカルの時刻(その PC のタイムゾーンの時刻)として扱われる
- だから UTC の時刻は aware で表すのがよく、今の UTC は
datetime.now(timezone.utc)で取るのがおすすめ utcnow()とutcfromtimestamp()は Python 3.12 で非推奨(deprecated、将来なくなる予定)
実際に、ずれを見てみます。どちらも「UTC の 2026-10-01 00:00」のつもりの時刻です。
t4_timestamp.py:
from datetime import datetime, timezone
print("この PC のタイムゾーン:", datetime.now().astimezone().tzinfo)
# どちらも「UTC の 2026-10-01 00:00」のつもり
naive_utc = datetime(2026, 10, 1, 0, 0) # utcnow() と同じく tzinfo なし
aware_utc = datetime(2026, 10, 1, 0, 0, tzinfo=timezone.utc)
print("naive.timestamp():", naive_utc.timestamp())
print("aware.timestamp():", aware_utc.timestamp())
print("差(秒):", aware_utc.timestamp() - naive_utc.timestamp())
print("naive.astimezone(UTC):", naive_utc.astimezone(timezone.utc))
print("aware.astimezone(UTC):", aware_utc.astimezone(timezone.utc))
この PC のタイムゾーン: 東京 (標準時)
naive.timestamp(): 1790780400.0
aware.timestamp(): 1790812800.0
差(秒): 32400.0
naive.astimezone(UTC): 2026-09-30 15:00:00+00:00
aware.astimezone(UTC): 2026-10-01 00:00:00+00:00
(今回の検証環境の Windows 10・Python 3.13.1、タイムゾーンは東京で実行した結果です)
32400秒は、ちょうど9時間。 札なしの UTC の0時を、timestamp()(1970年1月1日 UTC からの秒数に直すメソッド)が「東京の0時」として計算したので、9時間ずれました。 ドキュメントにも、naive な datetime はローカルの時刻とみなして変換する、と書かれています。
utcnow「UTC の0時です!」
timestamp「札がないので、東京の0時として数えますね」
utcnow「UTC って言ったのに……」
timestamp「言っただけで、書いてないので」
……口約束は、Python では通らない。
ずれの大きさは PC のタイムゾーンで変わります。UTC の PC(クラウドのサーバーによくある設定)なら、計算上は差が0秒になります。 つまり手元の PC では9時間ずれるのに、本番サーバーではずれない、という、いちばん見つけにくい形になりえます。
じゃあ全部のサーバーを東京時間にすれば、ずれ方がそろう?
そろいます。 そして海外のお客さんが来た日に、全部のずれ方が一斉に変わります。
非推奨の警告は、出ないこともある
「非推奨なら、警告が出るから気づくでしょ」と思いきや、そうとも限りません。
t3_utcnow.py:
from datetime import datetime, timezone
import helper_mod
a = datetime.utcnow() # __main__ から直接呼ぶ
b = helper_mod.now_utc_old() # 別モジュールの中で呼ぶ
c = datetime.now(timezone.utc) # おすすめの書き方
print("utcnow():", a.tzinfo)
print("helper :", b.tzinfo)
print("now(utc):", c.tzinfo)
helper_mod.py:
from datetime import datetime
def now_utc_old():
return datetime.utcnow()
python -X utf8 t3_utcnow.py
t3_utcnow.py:4: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
a = datetime.utcnow() # __main__ から直接呼ぶ
utcnow(): None
helper : None
now(utc): UTC
(今回の検証環境の Windows 10・Python 3.13.1 で実行した結果です。標準エラーの警告を先に載せ、ファイルのパスは短くしています)
警告が出たのは4行目、__main__(直接実行したスクリプト)の中の utcnow() だけ。 helper_mod の中の utcnow() は、同じように札なしの時刻を返したのに、警告は出ていません。
warnings のドキュメントによると、通常のビルドの既定の設定では、DeprecationWarning(非推奨の警告)は __main__ のコードから直接出たときだけ表示され、それ以外は無視されます(Python 3.7 から)。 ライブラリや、自分で作ったモジュールの中の utcnow() は、黙って札なしの時刻を返し続けるわけです。
helper_mod「ぼくも utcnow 使ってますけど、何も言われてません!」
警告係「本店(main)の中しか見回ってないので」
……支店の中は、ノーチェック。
テストや CI(コードを変えるたびに自動でテストする仕組み)では、警告をエラーにして見つけるのがおすすめです。
python -X utf8 -W error::DeprecationWarning t3_utcnow.py
Traceback (most recent call last):
File "t3_utcnow.py", line 4, in <module>
a = datetime.utcnow() # __main__ から直接呼ぶ
DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
(今回の検証環境の Windows 10・Python 3.13.1 で実行した結果です。ファイルのパスは短くしています)
-W error::DeprecationWarning は、DeprecationWarning を例外にするオプションです。 ただしこの結果のとおり、最初に見つかった1か所で止まります。モジュールの中まで一覧で見たいときは、あとで出てくる点検スクリプトのほうが向いています。
警告を全部エラーにしたら、テストが全部落ちた。
それは、今まで黙っていた警告が一斉にしゃべり出しただけです。 まずは DeprecationWarning だけに絞って、1つずつ片付けましょう。
中級:Windows で ZoneInfo が見つからない
UTC と日本時間だけなら timezone(timedelta(hours=9)) で足ります。 でも、夏時間のある地域を扱うなら zoneinfo(Python 3.9 からの標準モジュール)の出番です。そして Windows では、タイムゾーンのデータが OS に入っていないことがある、という落とし穴があります。
zoneinfo のドキュメントによると、
- 既定では、OS のタイムゾーンのデータを使う。なければ PyPI(Python のパッケージの置き場)の
tzdataパッケージを使う - Windows など、IANA のタイムゾーンデータベース(「Asia/Tokyo」のような名前と、各地の時差・夏時間の決まりをまとめたデータ)を持たないシステムがある。いろいろな OS で動かすなら
tzdataを依存関係に入れるのがおすすめ - どちらもなければ、
ZoneInfoの呼び出しはすべて ZoneInfoNotFoundError になる
今回の検証環境には、たまたま tzdata が入っていました。
t6_zoneinfo.py:
import importlib.util
import sys
from datetime import datetime
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
import zoneinfo
print(sys.version.split()[0], sys.platform)
print("tzdata パッケージ:", "あり" if importlib.util.find_spec("tzdata") else "なし")
print("TZPATH:", zoneinfo.TZPATH)
try:
tokyo = ZoneInfo("Asia/Tokyo")
print(datetime(2026, 10, 1, 9, 0, tzinfo=tokyo))
except ZoneInfoNotFoundError as e:
print("ZoneInfoNotFoundError:", e)
3.13.1 win32
tzdata パッケージ: あり
TZPATH: ()
2026-10-01 09:00:00+09:00
(今回の検証環境の Windows 10・Python 3.13.1 で実行した結果です)
TZPATH(OS のタイムゾーンのデータを探すフォルダの一覧)は、今回の検証環境では空でした。 つまり OS からは見つけられず、tzdata パッケージのおかげで動いています。
では tzdata がなかったら。 パッケージを消すかわりに、tzdata を読み込めないように細工して試しました。
t8_notzdata.py:
import sys
sys.modules["tzdata"] = None # tzdata が入っていない環境を再現する(検証用の細工)
from zoneinfo import ZoneInfo
import zoneinfo
print("TZPATH:", zoneinfo.TZPATH)
ZoneInfo("Asia/Tokyo")
TZPATH: ()
zoneinfo._common.ZoneInfoNotFoundError: 'No time zone found with key Asia/Tokyo'
(今回の検証環境の Windows 10・Python 3.13.1 で実行した結果です。長いトレースバックは最後の1行だけにしています)
ZoneInfo「Asia/Tokyo ですね。……地図がありません」
店長「東京だよ? 日本の」
ZoneInfo「地図がないので、日本がどこかも分かりません」
……自分の国の場所も分からない。Windows に地図帳を持たせるのが tzdata です。
対処は、tzdata を入れることです。
python -m pip install tzdata
(今回の検証環境には入っていたので、このコマンドは実行していません)
requirements.txt(使うパッケージの一覧)にも tzdata を書いておくと、別の Windows の PC や、OS のデータがない環境に持っていったときに困りません。
手元では動いたのに、同僚の PC では ZoneInfoNotFoundError。
手元の PC には、ほかのパッケージを入れたついでに tzdata が入っていた、という形がありえます。 「動いた」は「地図帳を持っていた」だけかもしれません。
地図帳、全員に配っておけば……
それが requirements.txt です。配布リストに書いておけば、全員の机に届きます。
中級:完成コード|時刻を「aware の UTC」にそろえる道具箱と、札なしを探す点検スクリプト
実務で使える形にまとめます。方針は入ってきたらすぐ aware の UTC にそろえる、札なしは黙って受け入れない、です。
timeutil.py:
"""時刻を「aware の UTC」にそろえるための小さな道具箱"""
from datetime import datetime, timezone, timedelta
JST = timezone(timedelta(hours=9), "JST")
def is_aware(dt: datetime) -> bool:
"""ドキュメントの定義どおり: tzinfo があり、utcoffset() が None でない"""
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
def ensure_aware(dt: datetime, assume=None) -> datetime:
"""naive なら assume のタイムゾーンとみなす。assume がなければエラーにする"""
if is_aware(dt):
return dt
if assume is None:
raise ValueError(f"タイムゾーンのない時刻です: {dt.isoformat()}")
return dt.replace(tzinfo=assume)
def to_utc(dt: datetime, assume=None) -> datetime:
"""aware の UTC に変換する(瞬間は変えない)"""
return ensure_aware(dt, assume).astimezone(timezone.utc)
def parse(text: str, assume=None) -> datetime:
"""ISO 8601 の文字列を読み、aware の UTC で返す(Z は Python 3.11 以降)"""
return to_utc(datetime.fromisoformat(text), assume)
def now_utc() -> datetime:
"""utcnow() の代わり"""
return datetime.now(timezone.utc)
さっきエラーになった3つの時刻を、これで並べ直します。
t10_use_timeutil.py:
from timeutil import JST, parse
rows = [
"2026-10-01T09:00:00+09:00",
"2026-10-01T00:30:00Z",
"2026-10-01 08:45:00",
]
try:
[parse(s) for s in rows]
except ValueError as e:
print("assume なし:", e)
times = sorted(parse(s, assume=JST) for s in rows)
for t in times:
print(t.isoformat(), "=", t.astimezone(JST).strftime("%H:%M JST"))
[parse(s) for s in rows]
except ValueError as e: print(“assume なし:”, e) times = sorted(parse(s, assume=JST) for s in rows) for t in times: print(t.isoformat(), “=”, t.astimezone(JST).strftime(“%H:%M JST”))” tabindex=”0″ role=”button” style=”box-sizing: border-box; margin: 8px !important; padding: 0px !important; font-size: 14px; font-weight: 500; white-space: nowrap; vertical-align: middle; cursor: pointer; user-select: none; appearance: none; border: 0px; border-radius: 6px; line-height: 20px; display: flex !important; position: relative; color: rgb(9, 105, 218); background-color: rgba(0, 0, 0, 0); box-shadow: none; transition: color 80ms cubic-bezier(0.33, 1, 0.68, 1), background-color 80ms cubic-bezier(0.33, 1, 0.68, 1), box-shadow 80ms cubic-bezier(0.33, 1, 0.68, 1), border-color 80ms cubic-bezier(0.33, 1, 0.68, 1); justify-content: center !important; align-items: center !important; width: 28px; height: 28px;”>
assume なし: タイムゾーンのない時刻です: 2026-10-01T08:45:00
2026-09-30T23:45:00+00:00 = 08:45 JST
2026-10-01T00:00:00+00:00 = 09:00 JST
2026-10-01T00:30:00+00:00 = 09:30 JST
(今回の検証環境の Windows 10・Python 3.13.1 で実行した結果です)
ポイントです。
assumeを渡さないと、札なしの時刻は ValueError で止まる。「どこの9時?」を聞き返す係assume=JSTを渡すと、CSV の札なしの時刻を日本時間とみなして札を付ける(replace)- そのあと全部 UTC に換算する(
astimezone)ので、並べ替えも引き算もできる fromisoformat()が末尾のZを読めるのは Python 3.11 から(ドキュメントの変更履歴より)。3.10 以前で使うなら、Zを+00:00に置き換えてから読む
timeutil「札のない時刻が来ました。どこの時刻ですか?」
店長「知らない。たぶん日本」
timeutil「では assume=JST と書いてください。書いた人の責任になります」
……ハンコを押させるタイプの道具箱。
assume を省略できないようにしたのは、わざとです。 「たぶん日本」を、コードに書いて残しておくためです。
札なしを量産している場所を探す
既存のコードで、札なしの時刻を作りがちな呼び出しを一覧にするスクリプトです。 ast(Python のコードを構文として読む標準モジュール)で、関数の呼び出しを見ています。
find_naive.py:
"""naive な datetime を作りがちな呼び出しを探す(簡易チェック。見つけたら人が確認する)"""
import ast
import sys
from pathlib import Path
ALWAYS = {"utcnow", "utcfromtimestamp"} # 3.12 で非推奨。常に naive を返す
NEED_TZ = {"now": 0, "fromtimestamp": 1} # tz がないか None だと naive(位置引数の番号)
def has_non_none_tz(node, position):
"""tz が省略されず、明示的な None でもないかを確かめる"""
if len(node.args) > position:
value = node.args[position]
return not (isinstance(value, ast.Constant) and value.value is None)
for keyword in node.keywords:
if keyword.arg == "tz":
value = keyword.value
return not (isinstance(value, ast.Constant) and value.value is None)
return False
def check(path):
tree = ast.parse(Path(path).read_text(encoding="utf-8"), filename=str(path))
for node in ast.walk(tree):
if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)):
continue
name = node.func.attr
if name in ALWAYS:
yield node.lineno, f"{name}() は naive を返します(3.12 で非推奨)"
elif name in NEED_TZ:
if not has_non_none_tz(node, NEED_TZ[name]):
yield node.lineno, f"{name}() の tz が省略または None です(naive になります)"
elif name == "today":
yield node.lineno, "today() は naive を返します"
for arg in sys.argv[1:]:
for p in sorted(Path(arg).rglob("*.py")) if Path(arg).is_dir() else [Path(arg)]:
for line, msg in check(p):
print(f"{p.as_posix()}:{line}: {msg}")
点検したファイル(sample_app/report.py):
import time from datetime import datetime, timezone started = datetime.utcnow() local = datetime.now() ok = datetime.now(timezone.utc) stamp = datetime.fromtimestamp(time.time()) stamp_ok = datetime.fromtimestamp(time.time(), tz=timezone.utc) day = datetime.today()
python -X utf8 find_naive.py sample_app
sample_app/report.py:4: utcnow() は naive を返します(3.12 で非推奨)
sample_app/report.py:5: now() の tz が省略または None です(naive になります)
sample_app/report.py:7: fromtimestamp() の tz が省略または None です(naive になります)
sample_app/report.py:9: today() は naive を返します
(今回の検証環境の Windows 10・Python 3.13.1 で実行した結果です)
6行目と8行目(None ではない tz を渡している行)は出てきません。 名前だけで判定する簡易チェックなので、date.today() のような別物も拾いますし、datetime を別名で読み込んだ書き方は見落とすことがあります。見つかった行は、人が確認してください。
find_naive「today() を見つけました!」
店長「それ、日付しか使ってないやつ」
find_naive「名前が today だったので」
……名札だけで職務質問する係。でも、見逃すよりはいい。
見つかった datetime.now() を、全部 now(timezone.utc) に置換すれば終わり?
画面に「今の日本時間」を出したいだけの場所まで UTC になって、時計が9時間遅れて見えます。 保存・比較に使う時刻は UTC、画面に出す時刻は表示の直前に astimezone で直す。置換の前に、その時刻の役目を見てください。
ここから先は上級者向け。読み飛ばしてもOKです。
上級:夏時間のある地域では、足し算も比較も「壁の時計」で数える
ZoneInfo で札を付けると、夏時間(DST、夏のあいだ時計を1時間進める制度)も正しく扱えます。 ただし、同じ tzinfo どうしの計算は、実際の経過時間ではなく壁の時計の数字で行われます。
ドキュメントによると、aware な datetime どうしの引き算と比較は、
- 両方の tzinfo が同じなら、tzinfo と fold(後で説明)を無視して、日時の数字どうしで計算する
- tzinfo が違えば、両方を UTC に直してから計算する
2026年11月1日は、ニューヨークの夏時間が終わる日です(深夜2時に時計を1時間戻す)。この日の0時から「3時間後」を計算します。
t9_dst.py:
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
NY = ZoneInfo("America/New_York")
start = datetime(2026, 11, 1, 0, 0, tzinfo=NY) # 夏時間が終わる日の0時
end = start + timedelta(hours=3)
print("start:", start)
print("end :", end)
print("end - start(同じ tzinfo):", end - start)
print("UTC で引き算 :", end.astimezone(timezone.utc) - start.astimezone(timezone.utc))
# 1:30 は2回ある
first = datetime(2026, 11, 1, 1, 30, tzinfo=NY)
second = first.replace(fold=1)
print("fold=0:", first, first.tzname())
print("fold=1:", second, second.tzname())
print("fold=0 == fold=1:", first == second)
start: 2026-11-01 00:00:00-04:00
end : 2026-11-01 03:00:00-05:00
end - start(同じ tzinfo): 3:00:00
UTC で引き算 : 4:00:00
fold=0: 2026-11-01 01:30:00-04:00 EDT
fold=1: 2026-11-01 01:30:00-05:00 EST
fold=0 == fold=1: True
(今回の検証環境の Windows 10・Python 3.13.1・tzdata ありで実行した結果です)
読み方です。
start + 3時間は、壁の時計で0時→3時。でも途中で1時間戻ったので、実際には4時間たっている- 同じ tzinfo どうしの
end - startは3時間。UTC に直して引くと4時間 - 1時30分は、この日2回来る。
fold(Python 3.6 から。0 が1回目、1 が2回目)で区別できて、オフセットも -04:00 と -05:00 で違う - それなのに、同じ tzinfo どうしの
==は True。1時間離れた2つの瞬間が「等しい」と判定される
fold=0「1時30分です。1回目の」
fold=1「1時30分です。2回目の」
Python「同じ札なので、同じ人です」
店長「……1時間ずれて出勤してきた、双子」
……札が同じだと、双子を見分けてくれない。
実際の経過時間(課金、タイムアウト、稼働時間の計算など)がほしいときは、UTC に直してから引く。 「翌日の同じ時刻」のように、壁の時計で数えたいときは、同じ tzinfo のまま足す。 どちらがほしいのかを、コードを書く前に決めておく必要があります。
夏時間のない日本なら、関係ない話?
Asia/Tokyo だけで閉じているなら、今の日本には夏時間がないので、この差は出ません。 でも海外のユーザー、海外のクラウドのリージョン、海外の取引先のログが1つでも入ってきたら、関係者になります。
じゃあ全部 UTC で計算すれば、双子問題も起きない?
起きません。UTC には夏時間がないからです。 これが「中では UTC、見せるときだけ現地時間」がよく勧められる理由の1つです。
上級:書き出すときに札を落とさない
最後は、ファイルやデータベースに時刻を書き出す話です。書き出した文字列に札(オフセット)が残っていないと、読み込んだ側でまた札なしくんが生まれます。
t7_format.py:
from datetime import datetime, timezone, timedelta
JST = timezone(timedelta(hours=9))
naive = datetime(2026, 10, 1, 9, 0)
aware = naive.replace(tzinfo=JST)
print(repr(naive.strftime("%Y-%m-%d %H:%M %z")))
print(repr(aware.strftime("%Y-%m-%d %H:%M %z")))
print(naive.isoformat())
print(aware.isoformat())
print(JST, aware.tzname())
print(datetime.fromisoformat("2026-10-01T00:00:00Z"))
'2026-10-01 09:00 '
'2026-10-01 09:00 +0900'
2026-10-01T09:00:00
2026-10-01T09:00:00+09:00
UTC+09:00 UTC+09:00
2026-10-01 00:00:00+00:00
(今回の検証環境の Windows 10・Python 3.13.1 で実行した結果です)
- naive の
%z(UTC からのずれを出す書式)は空文字になる。エラーにはならず、末尾に空白だけ残る isoformat()は、aware なら+09:00まで書き出す。naive だと何も付かないtimezone(timedelta(hours=9))の名前はUTC+09:00。「JST」と表示したければ、timezone(timedelta(hours=9), "JST")のように名前を渡す- 末尾が
Zの文字列は、Python 3.11 以降のfromisoformat()なら UTC の aware として読める
strftime「%z ですね。札がないので、空白にしておきました!」
店長「エラーにしてよ」
strftime「空白は、立派な出力です」
……空白を書き出して、仕事をしたことになっている。
書き出す側で aware にしておけば、isoformat() が札ごと書いてくれます。 ログやファイルの時刻は、オフセット付きの ISO 8601(2026-10-01T00:00:00+00:00 の形)にそろえておくと、次に読む人(未来の自分を含む)が「どこの9時?」と悩まずにすみます。
未来のおうどん「このログの 09:00、どこの9時?」
過去のおうどん「……(返事がない)」
過去の自分は、聞き返しても答えてくれません。書き出す瞬間が、札を付ける最後のチャンスです。
チェックリスト
- 今の時刻は
datetime.now(timezone.utc)(Python 3.11 以降ならdatetime.now(UTC)も可)で取っている utcnow()・utcfromtimestamp()を使っていない(Python 3.12 で非推奨)- 外から読み込んだ時刻は、読み込んだ直後に aware の UTC にそろえている
- 札なしの時刻に札を付けるときは
replace、別の地域に直すときはastimezoneと使い分けている - 札なしの時刻に
timestamp()・astimezone()を使うと、PC のタイムゾーンとみなされることを知っている - Windows で
zoneinfoを使うなら、tzdataを依存関係に入れている - 経過時間は UTC で引き、壁の時計で数えたいときだけ同じ tzinfo のまま計算している
- 書き出す時刻は、オフセット付きの ISO 8601 にしている
- テストで
-W error::DeprecationWarningを試し、隠れたutcnow()を探した
9個。出前の電話で聞くことより多い。
出前の電話で聞くのは「どこの、何時」の2つだけ。 このチェックリストも、突きつめれば全部「どこの時刻か、札を付けておく」の言いかえです。
1個にまとめてくれればよかったのに。
1個にまとめると「札を付けろ」です。 それだと、どこで付けるのかが分からなくて、結局このリストに戻ってきます。
まとめ
冒頭では、支店の「9時です!」を信じて、誰もいない店にうどんが届きました。
- naive(札なし)と aware(札付き)は比べられない。
==は必ず False、<と引き算は TypeError - 直すのはエラーの行ではなく、時刻を読み込んだ場所
- 札を付けるのは
replace、別の地域に直すのはastimezone utcnow()は UTC なのに札なし。多くのメソッドが PC のタイムゾーンの時刻とみなす。今の時刻はdatetime.now(timezone.utc)- 非推奨の警告は
__main__の中でしか出ない。テストでは-W error::DeprecationWarning - Windows で
ZoneInfoを使うならtzdata - 同じ tzinfo どうしの計算は壁の時計。経過時間は UTC で
店長「よし、店じゅうの時計に札を付けよう。キッチンタイマーにも」
キッチンタイマー「……ぼく、3分を数えるだけなんですけど」
……タイマーは、どこで鳴っても3分です。札が要るのは「いつ」を表す時刻で、「どれだけ」を表す長さ(timedelta)ではありません。
支店「9時に出前お願いします!」
店長「どこの9時?」
支店「+09:00 の9時です!」
……今度は、ちゃんと札が付いてきた。
時刻は「何時か」だけでなく「どこの何時か」まで書いて、はじめて1つの瞬間になる。数字だけの9時は、出前の住所に番地が書いていないのと同じです。
まずは今日、手元のコードで utcnow( を検索してみてください。 1つ見つけて datetime.now(timezone.utc) に直したら、今日の出前は、ちゃんと温かいうちに届きます🍜
参考資料
- Python ドキュメント:datetime — Basic date and time types — aware と naive の定義、naive と aware の比較・引き算の TypeError と
==が False(3.3 の変更)、同じ tzinfo どうしの計算と比較で tzinfo・fold が無視されること、utcnow()・utcfromtimestamp()の非推奨(3.12)とおすすめの書き方、naive がローカルの時刻として扱われること、timestamp()・astimezone()の naive の扱い(3.6)、replace(tzinfo=)とastimezone()の違い、fromisoformat()の変更(3.11)、UTCの別名(3.11)、fold(3.6)、timezoneが固定のずれだけを表すこと - Python ドキュメント:zoneinfo — IANA time zone support — Python 3.9 で追加、OS のデータと tzdata パッケージの順、Windows には IANA のデータベースがないことと tzdata を依存関係に入れるすすめ、ZoneInfoNotFoundError、夏時間の切り替わりでの fold
- Python ドキュメント:warnings — Warning control — 既定では DeprecationWarning が
__main__からのときだけ表示されること(3.7)、-Wオプションと error の動き
確認日:2026-10-01。

コメント