高校国語785655 views
世界の国560595 views
Computer365120 views
高校生物549842 views
LaTeX957300 views
高校日本史189857 views
高校物理158224 views
いろは2986023 views
中学英語808712 views
小学理科717236 views
Help
Tools

English

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 データのように「辞書として扱わなければならない」場面だ。

dataclass

独自のクラスとしてインスタンスを生成する。属性アクセスは user.name のようにドット記法。

TypedDict

あくまで 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 以降では RequiredNotRequired を使ってもっと直感的に書ける。

from typing import TypedDict, Required, NotRequired

class User(TypedDict):
    name: Required[str]
    age: Required[int]
    email: NotRequired[str]
    phone: NotRequired[str]
total=False による制御

全キーを一括でオプションにする。一部だけオプションにするには継承が必要。

Required / NotRequired

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 の特徴でもあり、限界でもある。