コンテンツにスキップ

Mypy

Pydanticはすぐにmypyとうまく連携します。

Pydanticにはmypyプラグインも付属しており、コードの型チェック機能を改善するpydantic固有の重要な機能がmypyに追加されています。

たとえば、次のスクリプトを考えてみます。

from datetime import datetime
from typing import List, Optional

from pydantic import BaseModel


class Model(BaseModel):
    age: int
    first_name = 'John'
    last_name: Optional[str] = None
    signup_ts: Optional[datetime] = None
    list_of_ints: List[int]


m = Model(age=42, list_of_ints=[1, '2', b'3'])
print(m.middle_name)  # not a model field!
Model()  # will raise a validation error for age and list_of_ints

特別な設定がなければ、mypyはmissing model field annotationをキャッチせず、Pydanticが正しく解析するlist_of_ints引数について警告します。

test.py:15: error: List item 1 has incompatible type "str"; expected "int"  [list-item]
test.py:15: error: List item 2 has incompatible type "bytes"; expected "int"  [list-item]
test.py:16: error: "Model" has no attribute "middle_name"  [attr-defined]
test.py:17: error: Missing named argument "age" for "Model"  [call-arg]
test.py:17: error: Missing named argument "list_of_ints" for "Model"  [call-arg]

しかし、with the plugin enabled、正しいエラーが表示されます。

9: error: Untyped fields disallowed  [pydantic-field]
16: error: "Model" has no attribute "middle_name"  [attr-defined]
17: error: Missing named argument "age" for "Model"  [call-arg]
17: error: Missing named argument "list_of_ints" for "Model"  [call-arg]

pydantic mypyプラグインを使用すると、フィールド名や型が変更された場合にmypyが間違いをキャッチすることを知っているので、恐れずにモデルをリファクタリングすることができます。

他にも利点があります!詳細は以下を参照してください。

Using mypy without the plugin

コードをmypyで実行するには、次のようにします。

mypy \
  --ignore-missing-imports \
  --follow-imports=skip \
  --strict-optional \
  pydantic_mypy_test.py

Strict Optional

コードで--strict-optionalを渡すためには、デフォルトとしてNoneを持つすべてのフィールドにOptional[]またはOptional[]のエイリアスを使用する必要があります(これはmypyでは標準です)。

Other Pydantic interfaces

Pydanticdataclassesvalidate_call decoratorもmypyでうまく動作するはずです。

Mypy Plugin Capabilities

Generate a signature for Model.__init__

  • 動的に決定されるエイリアスを持たない必須フィールドは、必須キーワード引数として含まれます。
  • Config.populate_by_name=Trueの場合、生成されたシグネチャはエイリアスではなくフィールド名を使用します。
  • Config.extra='forbid'で、動的に決定されるエイリアスを使用しない場合、生成されたシグネチャは予期しない入力を許可しません。
  • オプション: init_forbid_extra plugin settingTrueに設定されている場合、Config.extra'forbid'でなくても、__init__への予期しない入力によってエラーが発生します。
  • オプション: init_typed plugin settingTrueに設定されている場合、生成されたシグネチャはモデルフィールドの型を使用します(そうでない場合は、解析を可能にするためにAnyとして注釈が付けられます)。

Generate a typed signature for Model.model_construct

  • model_constructメソッドは、入力データが有効であることがわかっており、解析する必要がない場合に、__init__の代わりに使用できます。このメソッドは実行時に検証を行わないため、エラーを検出するには静的チェックが重要です。

Respect Config.frozen

  • Config.frozenTrueの場合、モデルフィールドの値を変更しようとするとmypyエラーが発生します。faux immutability

Generate a signature for dataclasses

  • @pydantic.dataclasses.dataclassで装飾されたクラスは、標準のPythonデータクラスと同様に型チェックされます。
  • @pydantic.dataclasses.dataclassデコレータは、the Config sub-classと同じ意味を持つconfigキーワード引数を受け入れます。

Respect the type of the Field's default and default_factory

  • defaultdefault_factoryの両方を持つフィールドは、静的チェック中にエラーになります。
  • defaultおよびdefault_factory値の型は、フィールドの型と互換性がなければなりません。

Warn about the use of untyped fields

  • 型に注釈を付けずにモデルにパブリック属性を割り当てると、mypyエラーが発生します。
  • 目的がClassVarを設定することである場合は、入力を使用してフィールドに明示的に注釈を付ける必要があります。ClassVar

Optional Capabilities:

Prevent the use of required dynamic aliases

  • warn_required_dynamic_aliasesplugin settingTrueに設定されている場合、Config.populate_by_name=Falseのモデルで動的に決定されるエイリアスまたはエイリアスジェネレータを使用すると、mypyエラーが発生します。
  • このようなエイリアスが存在すると、mypyは__init__を正しく型チェックできないので、これは重要です。 この場合、デフォルトですべての引数がオプションとして扱われます。

Enabling the Plugin

プラグインを有効にするには、mypy config fileのプラグインのリストにpydantic.mypyを追加します(mypy.ini,pyproject.toml,setup.cfg)。

開始するには、以下の内容のmypy.iniファイルを作成するだけです。

[mypy]
plugins = pydantic.mypy

Note

pydantic.v1モデルを使用している場合は、プラグインのリストにpydantic.v1.mypyを追加する必要があります。

このプラグインはmypyバージョン>=0.930と互換性があります。

詳細については、plugin configurationドキュメントを参照してください。

Configuring the Plugin

プラグイン設定の値を変更するには、mypy設定ファイルに[pydantic-mypy]というセクションを作成し、オーバーライドしたい設定のキーと値のペアを追加します。

プラグインのstrictnessフラグをすべて有効にしたmypy.iniファイル(およびその他のmypy strictnessフラグも)は、次のようになります。

[mypy]
plugins = pydantic.mypy

follow_imports = silent
warn_redundant_casts = True
warn_unused_ignores = True
disallow_any_generics = True
check_untyped_defs = True
no_implicit_reexport = True

# for strict mypy: (this is the tricky one :-))
disallow_untyped_defs = True

[pydantic-mypy]
init_forbid_extra = True
init_typed = True
warn_required_dynamic_aliases = True

mypy>=0.900の時点で、mypy configはmypy.iniではなくpyproject.tomlファイルに含めることもできます。 上記と同じ設定は次のようになります。

[tool.mypy]
plugins = [
  "pydantic.mypy"
]

follow_imports = "silent"
warn_redundant_casts = true
warn_unused_ignores = true
disallow_any_generics = true
check_untyped_defs = true
no_implicit_reexport = true

# for strict mypy: (this is the tricky one :-))
disallow_untyped_defs = true

[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = true

Note on --disallow-any-explicit

--disallow-any-explicitmypy設定(またはAnyを禁止するその他の設定)を使用している場合、BaseModelを拡張するときにno-any-explicitエラーが発生する可能性があります。これは、デフォルトでは、Pydanticのmypyプラグインがdef__init__(self, field_1:Any, field_2:Any, **kwargs:Any):のようなシグネチャを持つ__init__メソッドを追加するためです。

Why the extra signature?

Pydanticmypyプラグインは、フィールドアノテーションと一致しない型でモデルを初期化する際の型エラーを避けるために、def__init__(self, field_1:Any, field_2:Any, **kwargs:Any):のようなシグネチャを持つ__init__メソッドを追加します。 例えばModel(date='2024-01-01')はこのAnyシグネチャがなければ型エラーになりますが、Pydanticには文字列'2024-01-01'datetime.date型に解析する機能があります。

この問題を解決するには、Pydantic mypyプラグインの厳密なモード設定を有効にする必要があります。具体的には、[pydantic-mypy]セクションに次のオプションを追加します。

[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true

init_prohibit_extra=Trueの場合、**kwargsは生成された__init__シグネチャから削除されます。init_typed=Trueの場合、フィールドのAny型は実際の型ヒントに置き換えられます。

この設定により、Pydanticモデルでエラーを発生させることなく--disallow-any-explicitを使用することができます。ただし、この厳密なチェックによって、有効なPydanticユースケース(datetimeフィールドに文字列を渡すなど)が型エラーとしてフラグ付けされる可能性があることに注意してください。