Pythonのdatetimeでnaive/aware混在をレビューでどう見抜くか

Pythonの datetime は、timezone情報を持たないnaive datetimeと、timezone情報を持つaware datetimeを扱える。
便利な一方で、実務では naiveとawareの混在 が障害の入口になりやすい。

レビューで危ないのは、次のような実装だ。

  • datetime.now()datetime.now(timezone.utc) が混在している
  • DB保存時だけUTCにして、比較時はローカル時刻を使っている
  • API入力のtimezone有無を検証していない
  • replace(tzinfo=...) で変換したつもりになっている
  • テストがローカルtimezoneに依存している

この記事ではPythonの日時API一覧ではなく、
時刻の基準がコード上で一貫しているかをレビューで確認する観点を整理する。

まず止めたい実装

naiveとawareが混在する実装
from datetime import datetime, timezone


def is_expired(expires_at: datetime) -> bool:
    return expires_at < datetime.utcnow()


def create_token() -> dict[str, datetime]:
    return {
        "issued_at": datetime.now(timezone.utc),
        "expires_at": datetime.utcnow(),
    }
Comment
@Reviewer: `datetime.utcnow()` はtimezone情報を持たないnaive datetimeを返します。一方で `issued_at` はaware datetimeです。比較や保存境界で混在するため、UTC awareに統一してください。

このコードは、見た目にはすべてUTCを使っているように見える。
しかし datetime.utcnow() の戻り値はtimezone情報を持たない。

レビューでは、変数名に utc が入っているかではなく、
値そのものがtimezone情報を持っているかを確認する必要がある。

なぜ危ないのか

時刻の混在は、すぐに例外として出る場合もあれば、静かに誤判定になる場合もある。

起きやすい問題は次の通りである。

  • naiveとawareの比較で例外になる
  • ローカルtimezoneのサーバーだけ期限判定がずれる
  • DBから読んだ値とAPI入力値の基準が違う
  • daylight saving timeの境界でテストが不安定になる
  • ログや監査時刻がAPIレスポンスと一致しない

レビューで見るべきなのは、個々の datetime 呼び出しではない。
システム境界ごとに時刻基準が決まっているかである。

レビューで見たい3つの判断線

1. 内部表現をUTC awareに統一しているか

サーバー内部では、UTC aware datetimeに統一すると境界が読みやすい。
現在時刻を作る箇所も、ヘルパーに集約するとレビューしやすい。

UTC awareの現在時刻を作る例
from datetime import datetime, timezone


def utc_now() -> datetime:
    return datetime.now(timezone.utc)
Comment
@Reviewer: 現在時刻の生成が複数箇所に散っています。UTC awareに統一するため、`datetime.now(timezone.utc)` を返すヘルパーへ集約してください。

2. API入力でtimezone有無を検証しているか

APIから受け取る時刻文字列は、timezone付きとは限らない。
入力境界で検証せず内部に入れると、後段の比較処理で混在が露出する。

timezoneなし入力を拒否する例
from datetime import datetime, timezone


def parse_deadline(value: str) -> datetime:
    parsed = datetime.fromisoformat(value)
    if parsed.tzinfo is None or parsed.utcoffset() is None:
        raise ValueError("deadline must include timezone")
    return parsed.astimezone(timezone.utc)
Comment
@Reviewer: API入力のdatetime文字列をそのまま内部に渡しています。timezoneなし入力を拒否するか、仕様としてどのtimezoneで解釈するかを境界で明示してください。

3. replace(tzinfo=...) を変換として使っていないか

replace(tzinfo=timezone.utc) はtimezone情報を付け替えるだけで、時刻を変換しない。
既に別timezoneの時刻をUTCへ変換したいなら、astimezone(timezone.utc) を使う。

Comment
@Reviewer: `replace(tzinfo=timezone.utc)` は時刻変換ではなくtzinfoの付け替えです。入力値がローカル時刻なら誤ったUTCとして扱われるため、解釈元timezoneを明示して `astimezone` で変換してください。

レビューでは、timezone付与とtimezone変換を分けて読む。

改善例

入力境界でUTC awareへ正規化し、内部比較もUTC awareに統一する。

UTC awareに統一した実装
from datetime import datetime, timedelta, timezone


def utc_now() -> datetime:
    return datetime.now(timezone.utc)


def parse_api_datetime(value: str) -> datetime:
    parsed = datetime.fromisoformat(value)
    if parsed.tzinfo is None or parsed.utcoffset() is None:
        raise ValueError("datetime must include timezone")
    return parsed.astimezone(timezone.utc)


def is_expired(expires_at: datetime) -> bool:
    if expires_at.tzinfo is None or expires_at.utcoffset() is None:
        raise ValueError("expires_at must be timezone-aware")
    return expires_at.astimezone(timezone.utc) < utc_now()


def create_token(ttl_seconds: int) -> dict[str, datetime]:
    issued_at = utc_now()
    return {
        "issued_at": issued_at,
        "expires_at": issued_at + timedelta(seconds=ttl_seconds),
    }

この構造なら、レビューアーは次を確認しやすい。

  • 現在時刻の生成が1箇所に集約されている
  • API入力でtimezoneなしを拒否している
  • 内部比較がUTC aware同士になっている
  • replace で変換したつもりになる余地がない
  • 期限生成で基準時刻が1回だけ取得されている

DBとAPIの境界で確認したいこと

DBや外部APIは、それぞれdatetimeの扱いが違う。
レビューでは、境界ごとに変換方針を明示したい。

Comment
@Reviewer: DBから取得した `created_at` がnaive datetimeとして扱われています。DB driverがUTCとして返す契約なのか、アプリ側でtimezoneを付与する責務なのかを明示してください。
Comment
@Reviewer: APIレスポンスでtimezoneなし文字列を返しています。クライアントがローカル時刻として解釈する可能性があるため、UTC offset付きのISO文字列で返す設計にしてください。

時刻は内部だけで完結しない。
保存、受信、返却の境界で同じ基準を保てるかが重要である。

テストで時刻を固定しているか

datetime.now() を直接呼ぶ実装は、テストが現在時刻に依存しやすい。
時刻を注入できる形にすると、timezoneも含めて検証しやすい。

Comment
@Reviewer: テスト対象内で現在時刻を直接取得しているため、期限境界のテストが不安定になります。clock関数を注入し、UTC aware datetimeで固定できる構造にしてください。

期限切れ判定、日付集計、月末処理では特に確認したい。

レビュー観点チェックリスト

Python datetimeレビューの確認項目
  • datetime.utcnow()datetime.now() が混在していないか
  • 内部表現がUTC aware datetimeに統一されているか
  • API入力でtimezoneなしdatetimeを拒否または明示変換しているか
  • replace(tzinfo=...) を時刻変換として使っていないか
  • DB保存・取得時のtimezone契約が説明されているか
  • APIレスポンスにtimezone情報が含まれているか
  • 現在時刻をテストで固定できる構造か

まとめ

Pythonのdatetimeレビューでは、datetime を使っているかではなく、時刻の基準が揃っているかを見る。
特に naiveとawareの混在、UTC変換の責務、境界での検証 は早めに確認したい。

時刻バグは、サーバー環境や日付境界で突然見える。
レビューでは、正常系の日付表示よりも、基準時刻の一貫性を優先して読む。