pytz と zoneinfo の違いと移行ガイド
Python でタイムゾーンを扱うライブラリには、長年デファクトスタンダードだった pytz と、Python 3.9 で標準ライブラリに追加された zoneinfo があります。どちらもタイムゾーン情報を提供しますが、使い方に重要な違いがあります。
pytz の問題点
pytz は IANA タイムゾーンデータベースを Python で使えるようにした外部ライブラリです。2003 年から存在し、多くのプロジェクトで採用されてきました。しかし、pytz には直感に反する API 設計があります。
import pytz
from datetime import datetime
# 間違った使い方(よくある誤り)
jst = pytz.timezone('Asia/Tokyo')
dt = datetime(2024, 1, 1, 12, 0, tzinfo=jst)
print(dt)
# 2024-01-01 12:00:00+09:19
出力を見ると、オフセットが +09:00 ではなく +09:19 になっています。これは pytz が歴史的なタイムゾーン情報(1887 年以前の日本標準時)を返してしまうためです。
# 正しい使い方
jst = pytz.timezone('Asia/Tokyo')
dt = jst.localize(datetime(2024, 1, 1, 12, 0))
print(dt)
# 2024-01-01 12:00:00+09:00
pytz では tzinfo= での直接指定ではなく、localize() メソッドを使う必要があります。この非直感的な API が多くのバグの原因となってきました。
zoneinfo の登場
Python 3.9 で追加された zoneinfo モジュールは、標準の datetime API と自然に統合されます。
from zoneinfo import ZoneInfo
from datetime import datetime
# 直感的な使い方
jst = ZoneInfo('Asia/Tokyo')
dt = datetime(2024, 1, 1, 12, 0, tzinfo=jst)
print(dt)
# 2024-01-01 12:00:00+09:00
tzinfo= で直接指定しても正しいオフセットが適用されます。これは zoneinfo が datetime の設計に沿って実装されているためです。
localize() メソッドが必須で、tzinfo= 直接指定は誤動作の原因になる
標準の tzinfo= 指定がそのまま使え、datetime API と自然に統合されている
夏時間の扱い
夏時間(DST)を跨ぐ計算でも、両者の違いが現れます。
from datetime import datetime, timedelta
import pytz
# pytz での夏時間跨ぎ
eastern = pytz.timezone('US/Eastern')
dt = eastern.localize(datetime(2024, 3, 10, 1, 30))
dt_plus_2h = dt + timedelta(hours=2)
print(dt_plus_2h)
# 2024-03-10 03:30:00-05:00(間違い!-04:00 であるべき)
pytz では timedelta を加算しても DST 情報が更新されません。正しく計算するには normalize() が必要です。
dt_plus_2h = eastern.normalize(dt + timedelta(hours=2))
print(dt_plus_2h)
# 2024-03-10 04:30:00-04:00(正しい)
一方、zoneinfo では特別な処理は不要です。
from zoneinfo import ZoneInfo
from datetime import datetime, timedelta
eastern = ZoneInfo('US/Eastern')
dt = datetime(2024, 3, 10, 1, 30, tzinfo=eastern)
dt_plus_2h = dt + timedelta(hours=2)
print(dt_plus_2h)
# 2024-03-10 04:30:00-04:00(自動的に正しい)
移行の手順
既存のコードを pytz から zoneinfo に移行する際は、以下の対応が必要です。
| pytz | zoneinfo |
|---|---|
| pytz.timezone('Asia/Tokyo') | ZoneInfo('Asia/Tokyo') |
| tz.localize(dt) | dt.replace(tzinfo=tz) |
| tz.normalize(dt) | 不要(自動処理) |
| pytz.utc | datetime.timezone.utc |
移行時の注意点として、localize() を replace(tzinfo=...) に置き換えるだけでは不十分なケースがあります。曖昧な時刻(DST 切り替わり時)の扱いが異なるためです。
from zoneinfo import ZoneInfo
from datetime import datetime
# DST 終了時の曖昧な時刻(同じ時刻が2回現れる)
eastern = ZoneInfo('US/Eastern')
# fold=0 は最初の出現(夏時間側)
dt1 = datetime(2024, 11, 3, 1, 30, tzinfo=eastern, fold=0)
print(dt1, dt1.utcoffset())
# 2024-11-03 01:30:00-04:00 -1 day, 20:00:00
# fold=1 は2回目の出現(標準時側)
dt2 = datetime(2024, 11, 3, 1, 30, tzinfo=eastern, fold=1)
print(dt2, dt2.utcoffset())
# 2024-11-03 01:30:00-05:00 -1 day, 19:00:00
zoneinfo では fold 属性で曖昧な時刻を区別します。これは PEP 495 で導入された仕様です。
タイムゾーンデータの更新
pytz はパッケージ自体を更新することでタイムゾーンデータが更新されます。一方、zoneinfo はシステムの IANA データベース(多くの Linux では /usr/share/zoneinfo)を参照します。
# システムにタイムゾーンデータがない場合のフォールバック
# pip install tzdata
from zoneinfo import ZoneInfo
# tzdata パッケージがあれば、そこからデータを取得
jst = ZoneInfo('Asia/Tokyo')
Windows など、システムに IANA データがない環境では、tzdata パッケージをインストールすることでデータを利用できます。
移行すべきか
新規プロジェクトでは zoneinfo を使うべきです。Python 3.9 以上を使っているなら、標準ライブラリだけで完結し、直感的な API を利用できます。
zoneinfo を使用。外部依存がなく、datetime API と自然に統合される。
動作に問題がなければ急いで移行する必要はない。ただし、新しいコードでは zoneinfo を推奨。Python 3.8 以下のサポートが不要になったタイミングで移行を検討する。
pytz の作者自身も、pytz のドキュメントで Python 3.9 以降では zoneinfo の使用を推奨しています。