pythonのdatetimeのtzinfoについての話

pythonのdatetimeはnativeとawareという二つのものが存在します。端的にいうと
nativeとは純粋に日付と時間を表したもので年月日時分秒を表したものということでよいかと思います。
2023/4/1から2023/8/24まで何秒か?なんて計算をするときに有効です。

一方awareはその地域における時間で例えば日本で9時、イギリスで1時、アメリカで何時のようなもので、
その地域の壁時計時間が何時かなどという処理をするときに有効です。

何も考えずにdatetimeオブジェクトを作成するとnativeになります。tzinfoにtimezoneを表すオブジェクトを引き渡して作成をすると
awareになります

from datetime import datetime, timezone
datetime(2023,10,1) #native
datetime(2023,10,1, tzinfo=timezone.utc) # aware 

nativeとawareはその用途や目的が違うので相互に演算をすることが出来ません。演算しようとするとエラーとなります。

さておよそPCの時間というのは所謂epochで管理されています。epochというのは
1970/1/1 00:00:00を起源としそこからの経過秒数です。これは閏秒を考慮していないUTCでtime()関数の戻り値でもあります。

この閏秒を考慮していないというのが非常に快適で今現在のepochに3600*24を足せば必ず明日の同時刻になります。
UTCとのずれに「分」がない地域では3600で割り切れる時間は必ず正時になります。
日本はUTCから9時間進んでいるので3600*24で割ったあまりが3600*9ならばそれは夜中の0時ということになります。

以下awareなdatetimeについて主に話を進めます。pythonのtimezoneはtzinfoというクラスで管理されているのですが
このクラスはインスタンス化できない仮想クラスで長らく実装されたクラスは標準で datetime.timezone.utcしかありませんでした。
そのためpytzというモジュールを利用したりしていたのですが、最近ではzoneinfoというモジュールが追加されこの辺り使いやすくなっています。

試しにAsia/Tokyoでawareな時刻を作成してそのepochを取得してみたりtimedeltaを足したりして時間を進めてみたりしてみます。
datetimeからepochを得るにはtimestamp()メソッドを使います。
datetimeにtimedelta(seconds=3600)を足すと1時間先の時刻になることがtimestamp()の結果からわかると思います。

from zoneinfo import ZoneInfo
from datetime import datetime, timedelta

TZ = ZoneInfo('Asia/Tokyo')
delta = timedelta(hours=1)
d = datetime(2023, 3, 26, 0, 0, 0, tzinfo=TZ)
print(d, d.timestamp())
d += delta
print(d, d.timestamp())
d += delta
print(d, d.timestamp())
d += delta
print(d, d.timestamp())

実行結果

2023-03-26 00:00:00+09:00 1679756400.0
2023-03-26 01:00:00+09:00 1679760000.0
2023-03-26 02:00:00+09:00 1679763600.0
2023-03-26 03:00:00+09:00 1679767200.0

同じプログラムをAsia/TokyoではなくEurope/Zurichで動かしてみます。すると以下のような結果が表示されます。

2023-03-26 00:00:00+01:00 1679785200.0
2023-03-26 01:00:00+01:00 1679788800.0
2023-03-26 02:00:00+01:00 1679792400.0
2023-03-26 03:00:00+02:00 1679792400.0


3/26の03:00から+02:00となっているのがわかるかと思います。これはチューリッヒでは2023/3/26の深夜に夏時間に移行するからです。
そしてさらに詳しくみてみるとepochが1679792400.0の1時間後がまた1679792400.0になっています。つまりepoch的に全く進んでないことがわかります。

サマールールへの移行のルールを詳しく書くと

2023年は3/26の01:59の次は02:00ではなく03:00(そして時差+02:00)です。つまり上記の出力結果である

2023-03-26 02:00:00+01:00 1679792400.0

は存在しない時間です。ですがpythonでは出力されてしまいます。どうもpythonはawareであっても純粋にhourに1を足してしまうようで、その結果epochが進まないという事態になっているようです。
これは結構トラップな気がします。

個人的にはtimedelta(hours=1)をawareな時刻であっても足したらawareの時刻において1時間先になってほしいところですがpythonはそうはなっていないようです。

なおこのプログラムを以下のように書き換えepochで管理をするように務めると

ts =  1679785200
d = datetime.fromtimestamp(ts, tz=TZ)
print(d, d.timestamp())

ts += 3600
d = datetime.fromtimestamp(ts, tz=TZ)
print(d, d.timestamp())

ts += 3600
d = datetime.fromtimestamp(ts, tz=TZ)
print(d, d.timestamp())

ts += 3600
d = datetime.fromtimestamp(ts, tz=TZ)
print(d, d.timestamp())

以下のように期待した出力が得られます

2023-03-26 00:00:00+01:00 1679785200.0
2023-03-26 01:00:00+01:00 1679788800.0
2023-03-26 03:00:00+02:00 1679792400.0
2023-03-26 04:00:00+02:00 1679796000.0

これらの実験は全てpython3.9での実行結果です。