🐍
【Python】Pydantic, TypedDict で予約語をキーに使いたいときの対処法
Python には TypedDict というものがあり、これを使うと辞書型のオブジェクトの型を静的にチェックしてくれるようになります。TypeScript の type に似ています。
例
from typing import TypedDict
class Person(TypedDict):
name: str
age: int
person: Person = {
"name": "Alice",
"age": 30,
}
person["age"] = "30" # <- Type Error
最近は Pydantic のおかげで TypedDict の出番が少なくなっていますが、既存のコードが辞書型を使っていて、型チェックが効くようにしたい場合は TypedDict の方が低コストで行えます。
ただし、Python の予約語をキーにしたい場合、例えば from と to というキーを持つ辞書型の型を定義したい場合、うまくいきません。
TypedDict 予約語キー NG
from typing import TypedDict
class Range(TypedDict):
from: int # <- from が予約語なのでエラーになる
to: int
Pydantic でもこの問題は起こります。
Pydantic 予約語キー NG
from pydantic import BaseModel
class Range(BaseModel):
from: int # <- from が予約語なのでエラーになる
to: int
例えば API のスキーマ定義が from になっている場合などに困ることがあるかもしれません。
これらの問題の解決方法を紹介します。
TypedDict の場合
TypedDict は以下のように dict を渡して定義することもできます。
予約語キー OK
Range = TypedDict(
"Range",
{
"from": int,
"to": int,
},
)
これで一応解決できます。
ただし、ジェネリック型を使おうとすると、型パラメーターが渡せないのでこれではうまくいきません。(方法はあるのかもしれませんが、知りません。)
型パラメーター NG
from typing import Generic, TypedDict, TypeVar
T = TypeVar("T")
Range = TypedDict( # <- 型パラメーターが渡せない
"Range",
{
"from": int,
"to": int,
"something": T,
},
)
class SomeData(TypedDict, Generic[T]):
name: str
range: Range[T]
そういう場合は以下のようにすると良いです。
型パラメーター OK
from typing import Any, Generic, TypedDict, TypeVar
T = TypeVar("T")
_Range = TypedDict(
"_Range",
{
"from": int,
"to": int,
},
)
class Range(_Range, Generic[T]):
something: T
class SomeData(TypedDict, Generic[T]):
name: str
range: Range[T]
from をジェネリック型にしたい場合はどうすればいいんでしょうね。
Pydantic の場合
困るのは API 等の実装の時ぐらいだとも思うので、Pydantic 内では一貫して from_を使い、dict では一貫して from を使う、という方針でいきます。
以下のように実現できます。
pydantic
from pydantic import BaseModel, ConfigDict, Field
class Range(BaseModel):
model_config = ConfigDict(populate_by_name=True)
from_: int = Field(
...,
serialization_alias="from", # <- model_dump の出力のキーを from にする
validation_alias="from", # <- model_validate の入力のキーを from にする
)
to: int
_range = Range(
from_=1, # Field で alias="from" とするとここでエラー
to=10,
)
range = Range.model_validate({"from": 1, "to": 10})
print(range.from_) # <- 1
print(range.model_dump(by_alias=True)) # <- {'from': 1, 'to': 10}
Discussion