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
という選択肢
発展: TypedDict
やdataclasses
とよく比較されるライブラリにPydantic
があります。Pydantic
は、実行時の型チェックとバリデーション(検証)機能を提供します。
-
Pydantic
が適しているケース- 外部からの信頼できないデータの検証: APIリクエストのボディなど、外部から受け取るデータの形式が正しいか厳密にチェックしたい場合に非常に強力です。
- 設定管理: ファイルや環境変数から読み込んだ設定値が期待通りの型や値を持っているか検証するのに役立ちます。
TypedDict
が静的チェックのみであるのに対し、Pydantic
は実行時にチェックを行う点が大きな違いです。その分オーバーヘッドはありますが、データの信頼性が重要な場面では強力な選択肢となります。
まとめ
TypedDict
とクラスの使い分けは、データの出所とアプリケーション内での役割によって判断するのが良いでしょう。
-
TypedDict
: 静的型チェックが主目的。JSONなど辞書構造のデータをそのままの形で型安全に扱いたい場合に使う。 -
クラス (
dataclasses
): アプリケーション内部で使うデータオブジェクトを定義する場合に使う。データと振る舞いをカプセル化するのに適している。 -
Pydantic
: 外部データを扱う際に、実行時の厳密なバリデーションが必要な場合に使う。
これらのツールを適切に使い分けることで、Pythonコードの可読性、保守性、そして堅牢性を大幅に向上させることができます。
Discussion