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 の設計に沿って実装されているためです。

pytz

localize() メソッドが必須で、tzinfo= 直接指定は誤動作の原因になる

zoneinfo

標準の 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 に移行する際は、以下の対応が必要です。

pytzzoneinfo
pytz.timezone('Asia/Tokyo')ZoneInfo('Asia/Tokyo')
tz.localize(dt)dt.replace(tzinfo=tz)
tz.normalize(dt)不要(自動処理)
pytz.utcdatetime.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 の使用を推奨しています。