Python の辞書は柔軟で、どんなキーにどんな値でも入れられる。しかし dict[str, Any] では「何でも入る箱」としか表現できず、型チェックの恩恵がほとんど得られない。TypedDict を使えば、キーごとに値の型を指定した「構造を持つ辞書」を定義できる。
TypedDict の基本
TypedDict はクラス構文で辞書のスキーマを定義する。
from typing import TypedDict class User(TypedDict): name: str age: int email: str user: User = {"name": "Alice", "age": 30, "email": "alice@example.com"}
見た目はクラスだが、実行時には普通の dict として振る舞う。型チェッカだけがキーと値の型を検証してくれるため、実行時のオーバーヘッドはない。
user: User = {"name": "Alice", "age": "thirty", "email": "alice@example.com"} # mypy エラー: age は int であるべき
誤った型の値を入れると、mypy が静的にエラーを報告する。実行しなくてもバグを見つけられるのが型ヒントの強みだ。
なぜ dataclass ではなく TypedDict なのか
構造化されたデータを扱うなら dataclass でもよいのでは、と思うかもしれない。TypedDict が必要になるのは、API のレスポンスや JSON データのように「辞書として扱わなければならない」場面だ。
独自のクラスとしてインスタンスを生成する。属性アクセスは user.name のようにドット記法。
あくまで dict であり、user["name"] のようにブラケット記法でアクセスする。JSON との相互変換がそのままできる。
外部 API から受け取った JSON を json.loads でパースすると dict が返ってくる。その dict に型を付けたいときに TypedDict がぴたりとはまる。
import json from typing import TypedDict class ApiResponse(TypedDict): status: int message: str data: list[str] raw = '{"status": 200, "message": "OK", "data": ["a", "b"]}' response: ApiResponse = json.loads(raw) print(response["status"]) # 200 print(response["message"]) # OK
オプショナルなキー
辞書のキーの中には、存在しないかもしれないものがある。total パラメータで必須キーとオプションキーを分けられる。
from typing import TypedDict class Config(TypedDict, total=False): host: str port: int debug: bool config: Config = {"host": "localhost"} # port と debug は省略可
total=False を指定すると、すべてのキーがオプションになる。一部だけオプションにしたい場合は、必須部分とオプション部分を分けて継承する方法がある。
from typing import TypedDict class _UserRequired(TypedDict): name: str age: int class User(_UserRequired, total=False): email: str phone: str user: User = {"name": "Alice", "age": 30} # email, phone は省略可
Python 3.11 以降では Required と NotRequired を使ってもっと直感的に書ける。
from typing import TypedDict, Required, NotRequired class User(TypedDict): name: Required[str] age: Required[int] email: NotRequired[str] phone: NotRequired[str]
全キーを一括でオプションにする。一部だけオプションにするには継承が必要。
Python 3.11 以降で利用可能。キーごとに必須・オプションを個別に指定できる。
ネストした TypedDict
辞書の中に辞書がある構造も、TypedDict を入れ子にして表現できる。
from typing import TypedDict class Address(TypedDict): city: str zip_code: str class Company(TypedDict): name: str address: Address company: Company = { "name": "Acme Corp", "address": {"city": "Tokyo", "zip_code": "100-0001"}, }
ネストが深い JSON レスポンスを扱うとき、各レベルを TypedDict で定義しておけば、どの階層でも型チェックが効く。
関数の引数としての TypedDict
TypedDict は関数の引数や戻り値にも使える。
from typing import TypedDict class SearchParams(TypedDict): query: str page: int per_page: int def search(params: SearchParams) -> list[str]: print(f"Searching: {params['query']} (page {params['page']})") return [] search({"query": "python", "page": 1, "per_page": 20})
辞書をそのまま渡す API では、引数に TypedDict を指定することで「どのキーが必要か」を呼び出し側に明示できる。キーの typo も型チェッカが検出してくれる。
実行時の振る舞い
TypedDict は型チェッカ向けの仕組みであり、実行時にはただの dict として扱われる。isinstance チェックで TypedDict かどうかを判定することはできない。
from typing import TypedDict class User(TypedDict): name: str age: int user: User = {"name": "Alice", "age": 30} print(type(user)) # <class 'dict'> print(isinstance(user, dict)) # True
実行時のバリデーションが必要なら、pydantic や cattrs のようなライブラリと組み合わせることになる。TypedDict はあくまで静的解析のためのツールであり、実行時の安全性は別の仕組みで担保する必要がある。この割り切りが TypedDict の特徴でもあり、限界でもある。