🐍

Pydanticのmypyプラグインとalias_generatorで型チェックをすり抜ける

に公開

概要

  • Pydanticのmypyプラグインとalias_generatorの組み合わせで、余分なキーや必須フィールド不足がすり抜けていた
  • mypyでは両方を同時に検出する設定が存在しない
  • プラグインを外して余分なキーを検出するように変更。プラグインのメリットはほぼランタイムでカバーできる
  • tyなら両方同時に検出できるが、Stable版はまだ (2026年予定)

確認バージョン

  • Python: 3.14.2
  • Pydantic: 2.12.5
  • mypy: 1.19.1
  • ty: 0.0.13

発端

ある日コードレビューをしていると、明らかに間違っているのにCIを通過したコードを見つけた。

sample.py
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel

class Response(BaseModel):
    model_config = ConfigDict(alias_generator=to_camel)

    name: str
    age: int

Response(db_session="db_session", name="name")  # db_session?ageは?

REST APIのレスポンスにDBセッションを詰めていて、何事?と思ったら必須も足りていない。
あれ、こういうチェックのためにmypyがあるんじゃなかったっけ?

原因

調べてみると、以下のような挙動だった。

条件 余分なキー 必須フィールド不足
プラグインなし 検出 素通り
プラグインなし + alias_generatorあり 検出 素通り
プラグインあり 素通り 検出
プラグインあり + alias_generatorあり 素通り 素通り

つまり、mypyでは両方を同時に検出できる設定が存在しない。

プラグインがないとPydanticモデルの__init__シグネチャが認識されず、必須フィールドのチェックが効かない。
一方、プラグインを有効にすると、Pydanticのデフォルト設定extra="ignore"を尊重して余分なキーを許容するようになる。

さらに、alias_generatorを使うとプラグインがあっても必須フィールドのチェックが効かなくなる。
alias_generatorは実行時に評価される関数なので、静的解析では「どんな引数名になるか」を知る術がない。
プラグインは仕方なく**kwargsを許容するシグネチャを生成し、結果として全てのチェックが無効化されてしまう。

今回はalias_generatorを使っている状態でプラグインも有効だったので、両方素通りしていたわけだ。

$ mypy sample.py --config-file /dev/null  # alias_generatorあり + プラグインなし
error: Unexpected keyword argument "db_session" for "Response"  # 余分なキーのみ検出

Pydanticのmypyプラグインは必要か

プラグインのメリットは主に以下。

  • 必須フィールドのチェックができる
  • frozenモデルのフィールド変更を検出できる
  • モデル定義時に型なしフィールドを警告できる
  • Fielddefaultdefault_factoryの同時指定を検出できる
  • model_constructの引数を型チェックできる

ただし、今回のように余分なキーのチェックが緩くなるデメリットもある。
さらにalias_generatorと組み合わせると、せっかくの必須フィールドチェックも効かなくなる。

公式ドキュメントには以下のように書かれている。

Pydantic works well with mypy right out of the box.

However, Pydantic also ships with a mypy plugin that adds a number of important Pydantic-specific features that improve its ability to type-check your code.

https://docs.pydantic.dev/latest/integrations/mypy/

要するに「なくても動くが、あると便利」というスタンス。

結局どうしたか

プラグインを外した。

プラグインのメリットとして挙げられている機能を確認したところ、ほとんどがランタイムでもカバーされていた。

  • 「必須フィールドのチェック」→ ValidationError
  • 「frozenの変更検出」→ ValidationError
  • 「型なしフィールドの警告」→ PydanticUserError
  • defaultdefault_factoryの同時指定」→ TypeError

唯一model_constructの型チェックだけはランタイムで検出できないが、バリデーションをスキップするための特殊なメソッドなので使用頻度は低い。

これでプラグインありの時に素通りしていた余分なキーは検出できるようになった。
必須フィールドの欠落は依然としてmypyでは検出できないが、ランタイムでValidationErrorになるので許容することにした。

tyなら両方いける

Astral (Ruffやuvの開発元)が開発中の型チェッカーtyも試してみた。

https://github.com/astral-sh/ty

$ ty check sample.py

error[missing-argument]: No argument provided for required parameter `age`
error[unknown-argument]: Argument `db_session` does not match any known parameter

mypyでは片方しか検出できなかったが、tyなら両方いける。

以前はnot ready for production use.の記載があったが、2025年12月のBeta版リリースでその記述は消えていた。
とはいえStable版はまだ (2026年1月現在)なので、首を長くして待つことにする。

個人的にはもう少しググラビリティの高い名前にしてほしかったが...

おまけ: なぜtyだと検出できるのか

「そもそもなんでtyだと両方検出できるんだろう」と気になったので調べてみた。

@dataclass_transformの存在

PydanticのBaseModelは、メタクラスに@dataclass_transformデコレーター (PEP681)がついている。
これは「このクラスはdataclassみたいに扱ってね」と型チェッカーに伝えるためのもので、Python 3.11で追加された。

tyはこのデコレーターを見て、モデルのフィールド定義から__init__のシグネチャを推論している。
だから余分なキーも、必須フィールド不足も検出できる。

また、2026年1月時点でtyはPydanticサポート対応の途中だった。
つまりalias_generatorの存在を知らず、余計な許容もしないため、フィールド定義だけを見て型チェックしてくれる。

正式対応が入るとまた挙動が変わるかもしれないので、確認次第また内容を更新します。

Discussion