内藤 裕二/ 2025年 9月 4日/ 技術

こんにちは!内藤です!
ようやく札幌も気温と湿度が下がって過ごしやすくなってきました。
あっという間に冬が来るのだろうなぁ、と、いまからドキドキしています。

先日、DjangoのDateTimeFieldを持つModelを表示・編集する画面を作成していて、タイムゾーン問題に当たりました。
発生した問題自体は、画面上の時間が9時間早い日時になるというタイムゾーンがUTCになっているのが原因のありがちな問題です。
しかしながら、Djangoのタイムゾーン設定がどのように作用するのかイマイチわかっていなかったので、これを機に実験してみました。

やってみたこと

Djangoの USE_TZ, TIME_ZONEとデータベース(PostgreSql)のタイムゾーン設定を変更する
それぞれの場合で、

  • Model生成直後のDateTimeFieldのtzinfoと、strftimeの結果
  • データベースから読みだした直後のDateTimeFieldのtzinfoと、strftimeの結果がどうなるかを調査

TL;DR

  • データベース側のタイムゾーン情報は関係ない
  • Django側の USE_TZ の値によってDateTimeFieldの値が変わる
    • USE_TZ=Trueなら、内部的には常にtzinfo=UTCのAwareなDateTimeになる
    • USE_TZ=Falseなら、内部的には常にNaiveなDateTimeになる
  • USE_TZ=Trueの時、django.utils.localtime関数をかませると、TIME_ZONEで設定したタイムゾーンのAwareなDateTimeになる

前提知識

  • そもそもdatetimeのAwareとNaiveとは?
    • pythonの公式の説明がすべてです
    • めっちゃ単純化すると、タイムゾーン情報(tzinfoフィールド)が設定されているかどうかです

実験内容

実験コード

下記のようなDjangoのカスタムコマンドを作成し、それぞれの環境で実行します。
Docker環境も合わせた実験環境は、GitHubのリポジトリを参照してください。

from django.core.management.base import BaseCommand
from apps.main.models import SampleTimestamp

class Command(BaseCommand):
    help = "チェック: DateTimeFieldのtzinfoとstrftimeの挙動"

    def handle(self, *args, **options):
        self.stdout.write("\n=== 新規作成時 ===")
        obj = SampleTimestamp.objects.create(name="test")

        self._dump_datetime("生成直後", obj.created_at)

        self.stdout.write("\n=== DBから再取得後 ===")
        obj = SampleTimestamp.objects.get(id=obj.id)
        self._dump_datetime("DB取得後", obj.created_at)

    def _dump_datetime(self, label, dt):
        from django.utils.timezone import localtime

        self.stdout.write(f"{label}: {dt} (tzinfo={dt.tzinfo})")
        self.stdout.write(f"  strftime: {dt.strftime('%Y-%m-%d %H:%M:%S')}")

        try:
            local = localtime(dt)
            self.stdout.write(f"  localtime + strftime: {local.strftime('%Y-%m-%d %H:%M:%S %Z')}")
        except Exception as e:
            self.stdout.write(f"  localtime failed: {e}")

実験結果

実験結果として、それぞれの場合のtzinfoをまとめます。
PostgreSql側の設定は結果に影響がなかったので、パターンとして省きます。

DjangoのUSE_TZ=True, TIME_ZONE=Asia/Tokyoの場合

生成直後 生成直後のlocaltimeの結果 DB読み出し直後 DB読み出し直後localtimeの結果
UTC Asia/Tokyo UTC Asia/Tokyo

DjangoのUSE_TZ=True, TIME_ZONE=UTCの場合

生成直後 生成直後のlocaltimeの結果 DB読み出し直後 DB読み出し直後localtimeの結果
UTC UTC UTC UTC

DjangoのUSE_TZ=Falseの場合

生成直後 生成直後のlocaltimeの結果 DB読み出し直後 DB読み出し直後localtimeの結果
None 例外発生 None 例外発生

まとめ

DjangoのDateTimeFieldの値は、USE_TZ=Trueを設定しても、TIME_ZONEで設定したタイムゾーンではなくUTCが設定されています。
strftime等使用してシリアライズする時にTIME_ZONEで設定したタイムゾーンを使用したい場合、django.utils.timezone.localtimeを使用する必要があります。
(これを忘れると、画面側の日時だけ9時間早くなります・・・)

終わりに

公式ドキュメントに書いてある通りといえば、書いてある通りの挙動です。
実際に試してみて、ちょっとすっきりしました。

参照URL

DRF + MySQLでの動作について検証している方がいらっしゃいました。