Pydanticには暗黙的な型変換があると知った話

はじめに

こんにちは、データコネクタチームの韓です。 業務ではPythonを主に利用してデータ基盤の開発を行っています。PythonのデータバリデーションにおいてはPydanticというライブラリが有名で、私の業務でもよく利用しています。今回の記事では、実際にPydanticを使用してバリデーションを実装する際に直面した問題を再現しながら話したいと思います。

環境

  • Python 3.11.6

  • Pydantic 1.10.14

問題

サンプルコードは以下になります、バリデーションはmakerのidを0より大きい数字であることを保証しています。

from typing import Any
from pydantic import BaseModel, validator

class Maker(BaseModel):
    id: int
    name: str

class Car(BaseModel):
    maker: Any
    number: int

    @validator("maker")
    def validate_maker_id(cls, v):
        if v.id < 1:
            raise ValueError("id must be greater than 0")
        return v

# Valid
_maker = Maker(id=123, name="hoge")
Car(maker=_maker, number=1)

# Invalid
_maker = Maker(id=0, name="hoge")
Car(maker=_maker, number=1)
# pydantic.error_wrappers.ValidationError: 1 validation error for Car
# maker
#   id must be greater than 0 (type=value_error)

ここからがこの記事の本題です。

まずは二つインスタンスを作ります。片方はCarクラスのmakerフィールドにdictを指定、もう片方はMakerを指定します。結果として、dictを指定した方がエラーになりました。

# not dict
Car(maker=Maker(id=1, name='hoge'), number=2)

# dict
Car(maker=dict(id=1, name='hoge'), number=2)
-> AttributeError: 'dict' object has no attribute 'id'

「dict型には参照できるidがありません」と書かれていました。デバッガーで値の型を確認したら

(Pdb) type(v)
<class 'dict'>

makerの型はdictになっているので、コードを下記のように修正して

-        if v.id < 1:
+        if v.get("id") < 1:

さっきのエラーは解消されましたが、今度はMakerを指定した方でエラーが発生しました。

AttributeError: 'Maker' object has no attribute 'get'

二つのインスタンスが同時に生成できない状態になっています。

解決策

makerフィールドの型がAnyなため、指定した値がそのままフィールドに格納されています。そこで、Carクラスのmakerフィールドの型をAnyからMakerに変更すると

 class Car(BaseModel):
-    maker: Any
+    maker: Maker
     number: int

引数のmakerにdictを指定してインスタンス化した場合でもmakerフィールドの型がMakerとなっていました。

car = Car(maker=dict(id=1, name="hoge"), number=2)
print(type(car.maker))
-> <class '__main__.Maker'>

調べたところ、Pydanticには暗黙的な型変換があり、インスタンスを生成する時に、型が違っても可能であれば自動的に変換してくれます。 (ちなみに、PydanticV2バージョンの場合、strict=trueに設定すれば、この自動変換が無効になります)

docs.pydantic.dev

People have long complained about pydantic for coercing data instead of throwing an error. E.g. input to an int field could be 123 or the string "123" which would be converted to 123 While this is very useful in many scenarios (think: URL parameters, environment variables, user input), there are some situations where it's not desirable.

学び

今回の件で学んだことは、普段使っているライブラリについてしっかり理解することが重要だと感じました。