matobaの備忘録

和歌山と東京を往復しつつ活動するエンジニアの記録

Djangoのtimezone.nowに関するモヤモヤを調べる

Djangoのtimezone.nowを使っていてモヤモヤした。特に、「なぜ、UTCの時刻を返すのか」と思ったので、調べてみた。 その記録。

背景

Djangoにはtimezoneのutilsがあり、nowという関数を持っている。 これを実行すると次のようになる。

>>> from django.utils import timezone
>>> timezone.now()
datetime.datetime(2024, 3, 27, 20, 49, 31, 114299, tzinfo=datetime.timezone.utc)

また、Djangoプロジェクトにはsettingsでタイムゾーンが設定できる。 例えば次のような感じ。

TIME_ZONE = "Asia/Tokyo"

しかし、タイムゾーンを設定していても timezone.now() はUTC時刻を返す。 設定したタイムゾーンでの時刻にする場合は、 timezone.localtime() を使えることを知っていた。 なので、次に私はこんな風に書いてみた。

>>> timezone.localtime(timezone.now())
datetime.datetime(2024, 3, 28, 5, 55, 30, 810238, tzinfo=zoneinfo.ZoneInfo(key='Asia/Tokyo'))

めんどくさい。というか、私は何度も timezone.now() で設定したタイムゾーンの時刻が返ってくると勘違いしている。

「あれ?なんでUTCの時刻返ってきてるんだ?あ、そうか。localtimeという概念あった。」

みたいなことを毎回考えている。モヤモヤするので、もうちょっとこの辺の経緯に関する情報を調べてみる。

ドキュメントを読む

とりあえず、Djangoのtimezone.nowに関するドキュメントを読んでみよう。 5.0でのドキュメントである。

Django ユーティリティ (django.utils) | Django ドキュメント | Django

次の記載がある。

USE_TZ が False の場合、システムのローカルタイムゾーンで現在の時刻を表す、関連するタイムゾーンのない naive な日時(タイムゾーンの関連がない日時)になります。

翻訳前

If USE_TZ is True, this will be an aware datetime representing the current time in UTC. Note that now() will always return times in UTC regardless of the value of TIME_ZONE; you can use localtime() to get the time in the current time zone.

USE_TZという概念が関係ありそうだ。

そもそも、 timezone.localtime() のデフォルト

また、ドキュメントを読んでいる時点で

timezone.localtime() はデフォルトでnowを返すことに気づいた。ドキュメントにそう書いてある。

value が省略された場合, デフォルトは now() になります。

Django ユーティリティ (django.utils) | Django ドキュメント | Django

つまり、タイムゾーンを意識した現在時刻は、これで取得すれば良い。

>>> timezone.localtime()
datetime.datetime(2024, 3, 28, 6, 8, 8, 700251, tzinfo=zoneinfo.ZoneInfo(key='Asia/Tokyo'))

具体的なコードも見てみたが、本当にそのままnowが実行されている。

django/django/utils/timezone.py at 617bcf611f3daa796e4054ba041089ece30a32fc · django/django · GitHub

というわけで、私は timezone.now() ではなく timezone.localtime() をもっと意識して使うと良さそうだ。

USE_TZについて

ここまで調べてみて、最初のめんどくささはある程度低減された。 ただ、せっかくなので、タイトルの問をもう少し深掘りしたい。

timezone.now() のドキュメントで USE_TZ の話が出てくるので、この設定値が関係してそうだ。 その辺をもう少し調べてみる。

USE_TZ

デフォルトで日時がタイムゾーンを意識するかどうかを指定する真偽値です。これが True に設定されている場合、Django は内部的にタイムゾーンを意識した日時を使用します。

Settings | Django documentation | Django

この記載を見て思うこととして、そもそも、この設定は必要なのか?(わざわざ、タイムゾーンを意識しない、という設定をすることがあり得るのか?)と思った。というのも、私は、Djangoを使い始めてから USE_TZ の値を変更したことがない。「デフォルト値から変更しない設定値」という印象で使ってきた。

なので、この設定が導入された経緯を追いかけてみよう。

UZE_TZはいつ導入されたのか

ドキュメントに USE_TZ の記載があるので、バージョンいくつから導入されているのかを確認してみよう

Djangoのドキュメントで閲覧できる最も古いバージョンは、1.8 である。そのドキュメントを見てみると、すでにUSE_TZがある。

Settings | Django documentation | Django

そこで次のことがわかった

  • UZE_TZが指定されていない場合、 Falseで扱われる(つまり、タイムゾーンを意識せずに扱う)
  • django-admin startproject でプロジェクトを作るとUSE_TZ = True が追加される。

というわけで、 1.8 以前に導入されていたことが見えるし、上記の記載からなんとなく以下の状況が推測できた。

  • 最初は、USE_TZがなかった。そして、タイムゾーンを意識せずに扱っていた。
  • データベースに日時を入れたり、結局のところ、タイムゾーンを意識する必要性が高まってきた。というか常にタイムゾーンを意識した方が望ましい状況になってきた。
  • 後方互換性を意識して、新規プロジェクトではデフォルトでタイムゾーンを意識させるように調整が入った。

これは事実かどうかの裏どりはしていない。が、面倒になってきたので、私の調査はここで終えるとする。

timezone.nowがUTCを返す理由もこの辺の兼ね合いなんだろうなあ、と想像した。

USE_TZの今後

少し余談だが、Django 5.0のドキュメントに USE_TZ のデフォルト値はTrueで、古いバージョンではFalseにだった。との情報があった。

Changed in Django 5.0: In older versions, the default value is False.

Settings | Django documentation | Django

これも推測の裏どりをしていないが、やはりそもそもの話として、 USE_TZ という設定値必要か?という話があるんだろうな、と想像した。 デフォルト値がTrueになったことで、 デフォルトの settings.pyから USE_TZ は姿を消すことになるような気がする。

そうすると、この記事に書いたような疑問を解消するのは、少しハードルが上がるようにも思う。 だから、どうという話ではないですが。

終わり

今回は、ふと思ったことを調べてみました。

ではでは〜