🐍

PythonのTypedDictとクラスの使い分け

に公開

はじめに

Pythonで型ヒントを活用する中で、データ構造を定義する方法としてTypedDictとクラス(特にdataclasses)は頻繁に利用されます。両者は似た目的で使えますが、その背景にある設計思想や適したユースケースは異なります。

私自身、どちらを使うべきか迷う場面が多々ありました。この記事では、TypedDictとクラスのそれぞれの特徴を整理し、どのような基準で使い分けるべきか、具体的なコード例を交えながら解説します。

TypedDictとは

TypedDictは、typingモジュールで提供される機能で、辞書(dict)のキーとその値の型を静的に検査するために使われます。あくまで型チェッカー(Mypyなど)のための機能であり、実行時には通常の辞書として扱われる点が最大の特徴です。

from typing import TypedDict

class UserDict(TypedDict):
    name: str
    age: int
    is_active: bool

# 型チェッカーはこれを正しいと判断する
user: UserDict = {"name": "Alice", "age": 30, "is_active": True}

# ageの型が違うため、型チェッカーがエラーを報告する
# user_invalid: UserDict = {"name": "Bob", "age": "25", "is_active": False}

print(type(user))
# 出力: <class 'dict'>

TypedDictで定義したUserDictは、実行時には単なるdictであるため、isinstance(user, UserDict)のような型チェックはできません。

クラス(dataclasses)とは

一方、クラスはデータとそれを操作するメソッドをまとめた独自の型を定義するための仕組みです。特にPython 3.7以降で導入されたdataclassesを使うと、__init____repr__といったボイラープレートコードを自動で生成でき、データ保持を目的としたクラスを簡潔に記述できます。

from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int
    is_active: bool

    def greet(self) -> str:
        return f"こんにちは、{self.name}です。"

# Userクラスのインスタンスが生成される
user = User(name="Alice", age=30, is_active=True)

print(type(user))
# 出力: <class '__main__.User'>

print(user.greet())
# 出力: こんにちは、Aliceです。

こちらはdictとは異なり、Userという独立したクラスのインスタンスが生成されます。そのため、独自のメソッドを定義したり、属性アクセス(user.name)でデータを扱ったりできます。

使い分けの比較

では、両者をどのように使い分けるべきでしょうか。以下の比較表にまとめます。

特徴 TypedDict クラス (dataclasses)
実行時の型 dict 独自のクラスインスタンス
データアクセス d['key'] (辞書形式) obj.attribute (属性形式)
メソッド定義 不可 可能
実行時型チェック なし なし
主な用途 外部データ(JSON等)の型付け アプリケーション内部のデータオブジェクト
オーバーヘッド 非常に低い 低い

TypedDictが適しているケース

TypedDictは、データが本質的に「辞書」であり、辞書のまま扱いたい場合に最適です。

  • 外部APIのレスポンスなど、JSONライクなデータの型付け
    Web APIから受け取ったJSONデータは、Pythonでは辞書として扱われます。この構造を維持したまま型安全性を確保したい場合にTypedDictは非常に有用です。
  • 既存の辞書ベースのコードへの型適用
    すでに関数の引数や返り値として辞書を多用しているコードベースに、後から静的解析の恩恵を受けさせたい場合に適しています。

クラス(dataclasses)が適しているケース

クラスは、データを単なる入れ物としてではなく、アプリケーション内で責任と振る舞いを持つオブジェクトとして扱いたい場合に適しています。

  • アプリケーションのドメインモデル
    ユーザー、商品、注文など、アプリケーションの核となる概念を表現する場合。これらのオブジェクトは、データだけでなく、それに関連するビジネスロジック(メソッド)を持つことが多いため、クラスで定義するのが自然です。
  • 不変(Immutable)なデータ構造の作成
    @dataclass(frozen=True)を指定することで、インスタンス作成後に属性が変更できない不変なオブジェクトを簡単に作れます。これにより、意図しない状態変更を防ぎ、プログラムの堅牢性を高めることができます。

発展: Pydanticという選択肢

TypedDictdataclassesとよく比較されるライブラリにPydanticがあります。Pydanticは、実行時の型チェックとバリデーション(検証)機能を提供します。

  • Pydanticが適しているケース
    • 外部からの信頼できないデータの検証: APIリクエストのボディなど、外部から受け取るデータの形式が正しいか厳密にチェックしたい場合に非常に強力です。
    • 設定管理: ファイルや環境変数から読み込んだ設定値が期待通りの型や値を持っているか検証するのに役立ちます。

TypedDictが静的チェックのみであるのに対し、Pydanticは実行時にチェックを行う点が大きな違いです。その分オーバーヘッドはありますが、データの信頼性が重要な場面では強力な選択肢となります。

まとめ

TypedDictとクラスの使い分けは、データの出所とアプリケーション内での役割によって判断するのが良いでしょう。

  • TypedDict: 静的型チェックが主目的。JSONなど辞書構造のデータをそのままの形で型安全に扱いたい場合に使う。
  • クラス (dataclasses): アプリケーション内部で使うデータオブジェクトを定義する場合に使う。データと振る舞いをカプセル化するのに適している。
  • Pydantic: 外部データを扱う際に、実行時の厳密なバリデーションが必要な場合に使う。

これらのツールを適切に使い分けることで、Pythonコードの可読性、保守性、そして堅牢性を大幅に向上させることができます。

Discussion