自分のパソコンでは正しく動いていたプログラムが、サーバーに置いたとたん9時間ずれた時刻を出しはじめた。そんな場面に出くわしたことはありませんか。
私も昔、毎朝9時に動くはずのバッチ処理が、なぜか夕方6時に動いていた原因を半日かけて追いかけたことがあります。コードは一文字も間違っていませんでした。
犯人は、日時そのものが時差の情報を持っていなかったことです。この記事では、その仕組みと、標準ライブラリのzoneinfoを使った対処を順番に見ていきます。
日時には時差を持つものと持たないものがある¶
まず知っておきたいのは、Pythonの日時には2つの種類があるという点です。時差の情報を持っているものと、持っていないものです。
持っていないほうは素朴な日時と呼ばれます。何時何分という数字だけがあって、それがどこの時刻なのかはどこにも書かれていません。
実際に確かめてみましょう。手元のPython 3.11で実行した結果をそのまま載せます。
from datetime import datetime
from zoneinfo import ZoneInfo
naive = datetime(2026, 9, 14, 5, 0)
aware = datetime(2026, 9, 14, 5, 0, tzinfo=ZoneInfo('Asia/Tokyo'))
print(naive) # 2026-09-14 05:00:00
print(naive.tzinfo) # None
print(aware) # 2026-09-14 05:00:00+09:00
print(aware.utcoffset()) # 9:00:00
下の2行には+09:00が付きました。これが日本標準時、つまり世界標準時より9時間進んでいるという印です。
上のnaiveのほうはtzinfoがNoneのままです。時差を持たない日時は、それが東京の5時なのかロンドンの5時なのかを自分では知りません。
冒頭のバッチ処理がずれたのは、まさにこれが理由でした。私のパソコンは日本時間で動き、サーバーは世界標準時で動いていたのです。
同じdatetime(2026, 9, 14, 5, 0)という書き方でも、動かす場所によって意味する瞬間が9時間ずれる。これがいちばんの落とし穴です。
日付の計算そのものに不安がある方は、先にこちらを読んでおくと理解が早くなります。【関連記事】Pythonのdatetimeを完全攻略!日付の計算やタイムゾーンで迷わないために
混ぜて計算しようとするとエラーになる¶
Pythonは、この2種類を混ぜた計算を拒否します。親切な設計だと私は思っています。
さきほどの2つを引き算してみましょう。
print(aware - naive)
# TypeError: can't subtract offset-naive and offset-aware datetimes
手元で動かすと、このTypeErrorが出ました。エラーを見るとがっかりしますが、これは事故を止めてくれている合図です。
もし黙って計算されていたら、9時間ずれた答えが何食わぬ顔で出てきます。そのほうがずっと厄介です。
zoneinfoで地域を指定する¶
では本題です。zoneinfoは、世界の時差の情報をPythonから使えるようにしてくれるモジュールです。
Python 3.9から標準ライブラリに入りました。追加のインストールは、基本的には要りません。
使い方は簡単で、ZoneInfoに地域の名前を渡すだけです。
from datetime import datetime
from zoneinfo import ZoneInfo
tokyo = datetime(2026, 9, 14, 5, 0, tzinfo=ZoneInfo('Asia/Tokyo'))
print(tokyo) # 2026-09-14 05:00:00+09:00
これで、この日時は東京の5時だと自分で名乗れるようになりました。
名前は地域/都市の形で書く¶
渡す名前には決まった書き方があります。大陸や地域の名前と、代表的な都市の名前をスラッシュでつなぎます。
よく使うものを表にまとめました。
| 書き方 | 地域 | 世界標準時との差 |
|---|---|---|
Asia/Tokyo |
日本 | 常に+9時間 |
UTC |
世界標準時 | 基準 |
America/New_York |
アメリカ東部 | -5時間(夏は-4時間) |
Europe/London |
イギリス | 0時間(夏は+1時間) |
Asia/Shanghai |
中国 | 常に+8時間 |
アメリカやイギリスの行に、夏だけ差が変わると書いてあります。これがサマータイムで、後ほど詳しく見ていきます。
名前を間違えると、その場でエラーになります。存在しない地域を渡したときの挙動も確かめておきましょう。
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
try:
ZoneInfo('Asia/Tokyo/Shibuya')
except ZoneInfoNotFoundError as e:
print(e) # 'No time zone found with key Asia/Tokyo/Shibuya'
黙って別の時刻になるのではなく、きちんと止まってくれます。使える名前の一覧はavailable_timezones()で取り出せます。
Windowsではtzdataが必要になることがある¶
ひとつ注意点があります。zoneinfoは時差の情報そのものを持っていません。
動いている環境が持っている情報を読みに行く仕組みです。LinuxやmacOSには最初から入っているのですが、Windowsには入っていません。
そのため、WindowsでZoneInfoNotFoundErrorが出たときは、次のパッケージを入れれば解決します。
pip install tzdata
チームで開発するときは、最初から依存関係に入れておくほうが安全です。自分の環境だけ動いて他の人が動かない、という状況を避けられます。
UTCとの行き来はastimezoneで¶
時差を持たせたあとは、別の地域の時刻へ変換できるようになります。使うのはastimezone()です。
東京の朝5時が、世界の他の場所では何時なのかを出してみましょう。
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
jst = datetime(2026, 9, 14, 5, 0, tzinfo=ZoneInfo('Asia/Tokyo'))
print(jst) # 2026-09-14 05:00:00+09:00
print(jst.astimezone(timezone.utc)) # 2026-09-13 20:00:00+00:00
print(jst.astimezone(ZoneInfo('America/New_York'))) # 2026-09-13 16:00:00-04:00
東京の14日朝5時は、世界標準時では13日の夜8時でした。ニューヨークではまだ13日の夕方4時です。
日付まで変わっている点に注目してください。時差をまたぐと、時刻だけでなく日付もずれるので、日付だけで条件分岐を書くと事故のもとになります。
ここで大事なのは、astimezone()が指し示す瞬間を変えていないことです。同じ一瞬を、別の土地の時計で読み直しているだけです。
utcnow()はもう使わない¶
現在時刻を取るときにも、同じ落とし穴があります。昔からよく使われてきたutcnow()には問題がありました。
名前に反して、時差を持たない日時を返してしまうのです。
from datetime import datetime, timezone
print(datetime.utcnow().tzinfo) # None
print(datetime.now(timezone.utc).tzinfo) # UTC
上の行は世界標準時の値なのに、自分がUTCだと名乗れません。この日時を他の値と比べると、さきほどのTypeErrorか、もっと悪いことに9時間ずれた結果が返ってきます。
この危険さから、utcnow()はPython 3.12で非推奨になりました。これから書くコードではdatetime.now(timezone.utc)を使ってください。
なおdatetime.UTCという短い書き方もPython 3.11で加わりました。古いバージョンにも対応したいときは、timezone.utcのほうが安全です。
日本時間の現在時刻がほしいときは、datetime.now(ZoneInfo('Asia/Tokyo'))と書きます。引数なしのnow()は、動かす環境まかせになるので避けたほうが無難です。
サマータイムをまたぐと計算が合わなくなる¶
ここからは、少し意地悪な話をします。時差を持たせたあとでも、まだ落とし穴が残っているのです。
サマータイムのある地域では、時計の針が年に2回動きます。そのため、1日は必ず24時間とはかぎりません。
1日足したのに23時間しか進まない¶
アメリカのサマータイムは2026年3月8日に始まります。その前日から1日進めてみましょう。
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
ny = ZoneInfo('America/New_York')
start = datetime(2026, 3, 7, 12, 0, tzinfo=ny)
plus = start + timedelta(days=1)
print(start) # 2026-03-07 12:00:00-05:00
print(plus) # 2026-03-08 12:00:00-04:00
print(plus - start) # 1 day, 0:00:00
real = plus.astimezone(timezone.utc) - start.astimezone(timezone.utc)
print(real) # 23:00:00
時計の上では1日進んでいるのに、実際に流れた時間は23時間でした。時差が-05:00から-04:00に変わっているのが見えますね。
これはPythonの不具合ではありません。現地の人にとっては、本当に23時間しか経っていないのです。
どちらが正しいかは、何をしたいかで決まります。明日の正午に会いましょうという約束なら時計どおりで正解ですし、24時間後に処理を動かしたいなら世界標準時で計算すべきです。
正確に24時間後を求めたいときは、いったん世界標準時へ移してから足します。
exact = (start.astimezone(timezone.utc) + timedelta(days=1)).astimezone(ny)
print(exact) # 2026-03-08 13:00:00-04:00
現地では13時になりました。針が1時間進んだぶん、時計の表示も1時間ずれています。
定期実行の設定でこのあたりを意識しておきたい方は、こちらもあわせてどうぞ。【関連記事】Pythonで定期実行するには?cronとタスクスケジューラの基本
保存と表示を分けて考える¶
ここまでの話をふまえると、設計の方針はかなりはっきりしてきます。
保存は世界標準時、表示はその人の地域で¶
実務でよく使われるのは、保存するときは世界標準時にそろえ、画面に出すときだけ地域の時刻へ直すやり方です。
なぜなら、世界標準時には時計の針が動く日がないからです。並べ替えも引き算も、素直に計算できます。
方法ごとの向き不向きを整理してみます。
| 方法 | 時差の情報 | 向いている場面 |
|---|---|---|
| 時差なしの日時 | 持たない | 誕生日など、時刻を伴わない日付 |
timezone(timedelta(hours=9)) |
固定の差だけ | 日本国内だけで完結する処理 |
ZoneInfo('Asia/Tokyo') |
地域として持つ | 通常はこれを選ぶ |
ZoneInfo('UTC')/timezone.utc |
基準 | 保存、ログ、システム間のやりとり |
2行目の固定の差は、日本だけを相手にするなら実は問題なく動きます。日本にはサマータイムがないからです。
ただ、あとから海外向けの機能が加わったときに、この書き方は静かに壊れます。最初からZoneInfoで書いておくほうが、あとで困りません。
文字列にして受け渡しするときは、isoformat()が便利です。2026-09-14T05:00:00+09:00のように、時差まで含んだ形で出してくれます。
APIでやりとりするデータに日時を含めるなら、この形を選んでおくと相手側でも迷いません。【関連記事】PythonでJSONデータの扱いをマスター!API連携に必須のjsonモジュールの使い方
データベースに入れるときは、もう少し注意が要ります。SQLiteには日時専用の型がなく、文字列か数値として保存されるためです。
保存前に世界標準時へそろえておくと、あとから読み直したときに解釈で迷いません。【関連記事】Pythonからデータベースを操作するsqlite3の使い方を解説
実務で気をつけていること¶
私は10年ほどエンジニアとして開発に関わってきましたが、時差の不具合は原因にたどり着くまでがとにかく長い種類のバグだと感じています。数字そのものは正しく見えるので、疑う対象になりにくいのです。
冒頭のバッチ処理の件も、ログの時刻とユーザーからの問い合わせ時刻を突き合わせて、ようやく9時間という差に気づきました。直したのは、時刻を作る1行だけでした。
それ以来、日時を扱うときは入口で必ず時差を持たせると決めています。外から入ってきた日時は、受け取った直後に時差付きへ変換するという方針です。
もうひとつ、ログの時刻は世界標準時で出すようにしています。複数のサーバーが別々の地域で動いていると、時刻順に並べるだけで一苦労になるからです。
サーバーの時刻設定そのものが気になってきた方は、こちらが土台になります。【関連記事】そもそもサーバーって何?Pythonアプリを公開する前に知りたい基礎知識
まとめ¶
Pythonの日時には、時差を持つものと持たないものがあります。持たない日時は、どこの時刻なのかを自分では知りません。
zoneinfoのZoneInfoを使えば、地域の名前を渡すだけで時差を持たせられます。Python 3.9から標準で使え、Windowsではtzdataを入れれば動きます。
現在時刻はdatetime.now(timezone.utc)で取ります。utcnow()はPython 3.12で非推奨になったので、これからは使いません。
そして、サマータイムのある地域では1日が24時間とはかぎりません。正確な時間差がほしいときは、世界標準時に移してから計算してください。
まずはお手元のコードで、datetime.now()やutcnow()と書いている場所を探してみてはいかがでしょうか。ここまでお読みいただきありがとうございました。