概略/TLDR
python の type hint は
- 「動作を一切影響しない」
- 「あくまでコードの可読性のため」
とは危ない大勘違い!
pydantic などの package を使うと、type hint が プログラムの実行時の動作を影響することができる(type coercion)。
→ その挙動を抑えるにため、pydantic に strict mode が用意されている
→ ただ strict mode が抑えないケースもあるので、とにかく「type hint は動作を影響できる」と認識しておく方が良い
背景 type hint について
python の type hint とは、「runtime の挙動を影響しないもの」とは思いガチ
だって、type hint を導入した PEP484 が type hint を「annotation」 と呼び、以下のように説明する:
While these annotations are available at runtime through the usual annotations attribute, no type checking happens at runtime. Instead, the proposal assumes the existence of a separate off-line type checker which users can run over their source code voluntarily.
これは正しいが、PEP484 の3段落目の言う通り、3rd party Libraries は hint を runtime で参照して良いので、「type hint はあくまで読者のため」とかは認識してはいけない!
投稿を書くきっかけ(ハマったエピソード)
- ある関数の戻り値を、pydantic の model にしている
- model は type が
SubModel
の fieldsub
を持っている - 条件によって、
sub
の type を type hint 通りで設定したり、違う type (空辞書)で設定したりする
from pydantic import BaseModel
class SubModel(BaseModel):
optional_field: int | None = None
class ReturnValueModel(BaseModel):
sub: SubModel
def get_pydantic_model(sub_as_expected_type: bool) -> ReturnValueModel:
if sub_as_expected_type:
return ReturnValueModel(sub=SubModel())
else:
return ReturnValueModel(sub={})
勘違いして期待した挙動
sub を 空辞書と設定している場合は、戻り値の sub
field が、 {}
となること
→ type が Sub
でなく、dict
か Object
実際の挙動:
sub
の type が必ず SubModel
となる
pydantic_model = get_pydantic_model(sub_as_expected_type=False)
print(type(pydantic_model.sub))
# OUTPUT: "<class '__main__.SubModel'>"
説明
pydantic は、 type coercion を行う。
(つまり、c 言語を書く人にとってとても恐ろしい暗黙変換を意図的にやっている)
可能な範囲内、model の field に与えられた値を、field の type に変換している。
- 例:
"1"
がint
として定義されている field に渡しても良い - 例:(上記のシナリ) 全ての field が optional、かつdefault が定義されている model (例では
SubModel
) である field (sub
) に、空辞書にを設定して良い
type coercion を避けたいなら、strict mode は対応可能 ※ある程度
pydantic は strict mode という機能がついており、そを使えば大半の type coercion を抑制することができる。
色んな使い方がある:
field 単位で設定できる: 以下の抜粋を pydantic の document から引用:
from pydantic import BaseModel, Field, ValidationError
class AnotherUser(BaseModel):
name: str
age: int = Field(strict=True)
n_pets: int
OR
from typing_extensions import Annotated
from pydantic import BaseModel, Strict, ValidationError
class User(BaseModel):
name: str
age: int
is_active: Annotated[bool, Strict()]
model 単位で設定できる: ConfigDict
ただし、上記書いた例のような場合は、strict モードが効かない
strict mode にしても、以下の type coercion が行われる
- JSON からの validation※ における UUID type のfield は、
str
が変換される (pydantic の doc の例を参照) - 例:(上記のシナリ) 全ての field が optional、かつdefault が定義されている model (例では
SubModel
) である field (sub
) に、空辞書が model に変換される
※ 「JSON からの validation」は、Model.model_validate_json()
で行った validation。
(Model.model_validate_json
の返却ちが Model の instance)
結論
- standard library のみを使ってれば、type hint 実行時の動作を影響しない
- 3rd party libraries は type hinto を runtime 参照して良いので、「type hint は影響ない」とは誤っており、バッグを招く認識
- pydantic の type hint による type coercion はある程度抑えられるが、どうしても避けられないケースがるので、留意する方が良い