この記事のポイント

  • Optional・Union型設計の崩壊ポイントをレビューできる
  • null許容設計と多重Union設計のミスを見抜ける
  • 責務整理と設計意図が型に正しく反映されているかを読む力がつく

そもそもOptionalとUnionとは

Pythonの型ヒントでは、以下の2つが頻繁に登場します。

from typing import Optional, Union

Optional[int]  # intまたはNone
Union[int, str]  # intまたはstr

どちらも「複数の型が許容されること」を型レベルで表現しますが、以下の違いがあります。

  • Optional[X] は Union[X, None] の糖衣構文
  • Union は任意の複数型の混在を許容

便利な反面、設計意図の曖昧化・責務崩壊・読みづらさを引き起こしやすく、レビュー観点として非常に重要です。

なぜこれをレビューするのか

以下のような設計崩壊が現場で頻発します。

  • Optionalを「後でとりあえず入れた」型修正
  • Union爆発による型地獄
  • None許容責務の不明瞭化
  • 呼び出し側が都度型分岐に苦しむ設計臭

レビューアーは「型定義=設計意図の写像」として読みます。

レビューアー視点

  • None許容が妥当か
  • Union要素数が抑制されているか
  • 呼び出し側の分岐負荷が適切か
  • データ設計と型定義が一致しているか
  • None表現の責務移譲が整理されているか

開発者視点

  • Optionalは「存在しないことが正当」な責務だけに使う
  • Unionは業務上の状態・分類と一致させる
  • None許容は呼び出し側の分岐コストを考慮
  • DTO層と業務層で型を分離整理
  • Union濫用を避け型安全性維持

良い実装例

なぜこの実装が良いのか

  • Optional使用箇所が存在許容責務に限定
  • Union使用箇所が状態区分責務と一致
  • 呼び出し側が型分岐不要
  • データ設計と型設計が一致
# api_request_log_model.py

from dataclasses import dataclass
from typing import Optional, Literal

@dataclass
class ApiRequestLog:
    request_id: int
    endpoint: str
    client_ip: str
    response_code: int
    requested_at: str
    archived_at: Optional[str]  # アーカイブ有無は業務上Optionalで妥当

ResponseStatus = Literal["SUCCESS", "FAIL", "TIMEOUT"]

@dataclass
class ApiResponse:
    request_id: int
    status: ResponseStatus
    message: Optional[str]
補足

「そもそも存在しない可能性が業務上存在するか?」をOptional導入時の判断基準としています。
Literal活用でUnionの型爆発も防いでいます。

レビュー観点

  • Optionalは「存在不定」責務に限定されているか
  • Union要素が業務状態表現と一致しているか
  • 呼び出し側が型分岐地獄に陥っていないか
  • DTO層と業務層の型責務が分離されているか
  • None許容が過剰でないか

良くない実装例: ケース1(Optional乱用によるnull地獄)

# bad_optional_everywhere.py

from dataclasses import dataclass
from typing import Optional

@dataclass
class ApiRequestLog:
    request_id: Optional[int]
    endpoint: Optional[str]
    client_ip: Optional[str]
    response_code: Optional[int]
    requested_at: Optional[str]
@Reviewer
request_idなど存在保証すべき項目にまでOptionalを付与しています。責務不明瞭化となります。

問題点

  • 必須項目にまでOptional適用
  • データ責務と型が乖離
  • 呼び出し側のnullチェック負荷爆発

改善例

# good_optional_minimal.py

from dataclasses import dataclass
from typing import Optional

@dataclass
class ApiRequestLog:
    request_id: int
    endpoint: str
    client_ip: str
    response_code: int
    requested_at: str
    archived_at: Optional[str]  # Optionalは業務妥当箇所のみ

Optionalは「なければ正常」「必須なら絶対不要」が原則。レビューではこの線引きを読む技術が重要です。

良くない実装例: ケース2(Union型の爆発)

# bad_union_explosion.py

from typing import Union

ResponseStatus = Union[str, int, None, bool]
@Reviewer
状態表現として用途不明のUnionが定義されています。業務状態に沿った限定表現に整理してください。

問題点

  • Unionに無関係な型が混在
  • 呼び出し側で都度型判定負担
  • 業務状態表現が型定義から読めない

改善例

# good_union_literal.py

from typing import Literal

ResponseStatus = Literal["SUCCESS", "FAIL", "TIMEOUT"]

業務状態と一致する列挙的型表現に整理。Literal活用でUnion型爆発を予防できます。

良くない実装例: ケース3(OptionalとUnionの混在肥大化)

# bad_optional_union_mix.py

from typing import Optional, Union

@dataclass
class ApiResponse:
    request_id: int
    status: Union[str, int, None]
    message: Optional[Union[str, None]]
@Reviewer
Unionの中にOptionalが混在し、型判定が極めて困難化しています。責務分離と型簡素化が必要です。

問題点

  • OptionalとUnionの重複混在
  • None含有責務が曖昧
  • 呼び出し側の型分岐地獄化

改善例

# good_union_optional_separation.py

from typing import Optional, Literal
from dataclasses import dataclass

ResponseStatus = Literal["SUCCESS", "FAIL", "TIMEOUT"]

@dataclass
class ApiResponse:
    request_id: int
    status: ResponseStatus
    message: Optional[str]

Optionalで「値があるかないか」、Unionで「状態が何種類あるか」を分離する設計が原則。混在肥大化はレビューで即指摘対象になります。

観点チェックリスト

まとめ

Optional・Union型は設計者のデータ責務整理力を如実に反映する指標です。
レビューアーは型定義そのものを読み解き、

  • なぜOptionalが必要か
  • Union要素数は妥当か
  • 呼び出し負荷はどう影響するか

を一貫して確認していくことで、設計崩壊前に型設計ミスを止めることができます。
Optional/Unionレビューは現場育成教材として極めて実戦効果が高い領域です。