TypedDict で辞書に構造を持たせる
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 の特徴でもあり、限界でもある。











