pydanticを触ってみる
はじめに
業務で AWS (boto3) から取得したデータをデータベースに格納するスクリプトを Python で書くタスクをしました。 その際 AWS から取得したデータをバリデーションして型安全にしたいと思い pydantic を導入してみたので備忘録を書きます。
今回使用したコードは以下に置いています。
https://github.com/kntks/blog-code/tree/main/2023/10/python-pydantic
pydantic とは
https://docs.pydantic.dev/latest/
Python のデータバリデーションツールです。
以下のように、dict 型のデータを自分で定義したクラスにマッピングしてくれます。 ※公式のExampleです。
from datetime import datetime
from pydantic import BaseModel, PositiveInt
class User(BaseModel): id: int name: str = 'John Doe' signup_ts: datetime | None tastes: dict[str, PositiveInt]
external_data = { 'id': 123, 'signup_ts': '2019-06-01 12:22', 'tastes': { 'wine': 9, b'cheese': 7, 'cabbage': '1', },}
user = User(**external_data)
print(user.id)
Python 3.7 から追加された dataclass
にも対応しています。
基本的な使い方や説明はドキュメントを読んだ方が良いので、ここでは触れません。
モチベーション
なぜ pydantic を入れたかというと、pythno の dict 型に使いづらさを感じていたからです。
たとえば、dict型から value を取り出したいとき、[]
を使うか、get
メソッドを使うことでデータを取得できます。
>>> hoge = {"a":"b"}>>> hoge["a"]'b'>>> hoge.get("a")'b'
しかし、存在しない key を指定した場合、エラーが発生するか、None
が返されます。
>>> hoge["aaa"]Traceback (most recent call last): File "<stdin>", line 1, in <module>KeyError: 'aaa'>>> hoge.get("aaa")# 何も出ない>>> print(hoge.get("aaa"))None
get
メソッドは第2引数にデフォルト値を設定することができるので問題ないように感じますが、dict型をプロジェクト内で使い回していると値を参照するたびに default 引数を使用するしなければなりません。
>>> hoge.get("aaa", "default")'default'
dict 型のこういったところに使いづらさを感じたため pydantic を使いました。
- https://docs.python.org/ja/3/tutorial/datastructures.html#dictionaries
- https://docs.python.org/ja/3/library/stdtypes.html#mapping-types-dict
バージョン
バージョン | |
---|---|
Python | 3.11.4 |
pydantic | 2.4.2 |
環境構築
$ python3 -m venv venv$ source venv/bin/activate
必要なモジュールをインストールします。
$ pip install pydantic
検証
とある API からデータの取得を想定して、以下の関数を定義しています。
def from_example_api(): return { "key1": "a", "key2": 1, "key3": { "key4": "c", "key5": "d" } }
データクラスを使ったサンプル
dataclass を使って簡単なデータをバリデーションしてみました。
from pydantic.dataclasses import dataclass
@dataclassclass Key3: key4: str key5: str
@dataclassclass Example: key1: str key2: int key3: Key3
def from_example_api(): return { "key1": "a", "key2": 1, "key3": { "key4": "c", "key5": "d" } }
if __name__ == "__main__": res = from_example_api() example = Example(**res)
print(example.key1) print(example.key2) print(example.key3)
ここではとくに問題なさそうです。
$ python example1.pya1Key3(key4='c', key5='d')
エラーを出してみる
今度は key2
を str 型にしてみます。本来 key2
の型は int なのでエラーが出るはずです。
from pydantic.dataclasses import dataclass
@dataclassclass Key3: key4: str key5: str
@dataclassclass Example: key1: str key2: int key3: Key3
def from_example_api(): return { "key1": "a", "key2": 1, "key2": "asdfasffadsgdgerg", "key3": { "key4": "c", "key5": "d" } }
if __name__ == "__main__": res = from_example_api() example = Example(**res)
print(example.key1) print(example.key2) print(example.key3)
意図した通りにエラーが出ました。
$ python example1.pyTraceback (most recent call last): File "/Users/xxx/blog-code/2023/10/python-pydantic/example1.py", line 27, in <module> example = Example(**res) ^^^^^^^^^^^^^^ File "/Users/xxx/blog-code/2023/10/python-pydantic/venv/lib/python3.11/site-packages/pydantic/_internal/_dataclasses.py", line 132, in __init__ s.__pydantic_validator__.validate_python(ArgsKwargs(args, kwargs), self_instance=s)pydantic_core._pydantic_core.ValidationError: 1 validation error for Examplekey2 Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='asdfasffadsgdgerg', input_type=str] For further information visit https://errors.pydantic.dev/2.4/v/int_parsing
余分なデータがある場合
今度は key6
を追加してみました。
from pydantic.dataclasses import dataclass
@dataclassclass Key3: key4: str key5: str
@dataclassclass Example: key1: str key2: int key3: Key3
def from_example_api(): return { "key1": "a", "key2": 1, "key3": { "key4": "c", "key5": "d" }, "key6": "fasdfadf" }
if __name__ == "__main__": res = from_example_api() example = Example(**res)
print(example)
実行してみると問題なく動きました。
$ python example2.pyExample(key1='a', key2=1, key3=Key3(key4='c', key5='d'))
必要のない key が存在した場合、エラーを出すには ConfigDict
の extra
を設定する必要があります。
from pydantic.dataclasses import dataclassfrom pydantic import ConfigDict
config = ConfigDict(extra="forbid")
@dataclassclass Key3: key4: str key5: str
@dataclass() @dataclass(config=config)class Example: key1: str key2: int key3: Key3
def from_example_api(): return { "key1": "a", "key2": 1, "key3": { "key4": "c", "key5": "d" }, "key6": "fasdfadf" }
if __name__ == "__main__": res = from_example_api() example = Example(**res)
print(example)
dataclass で定義していない key6
の箇所でエラーが出ました。
$ python example2.pyTraceback (most recent call last): File "/Users/xxx/blog-code/2023/10/python-pydantic/example2.py", line 31, in <module> example = Example(**res) ^^^^^^^^^^^^^^ File "/Users/xxx/blog-code/2023/10/python-pydantic/venv/lib/python3.11/site-packages/pydantic/_internal/_dataclasses.py", line 132, in __init__ s.__pydantic_validator__.validate_python(ArgsKwargs(args, kwargs), self_instance=s)pydantic_core._pydantic_core.ValidationError: 1 validation error for Examplekey6 Unexpected keyword argument [type=unexpected_keyword_argument, input_value='fasdfadf', input_type=str] For further information visit https://errors.pydantic.dev/2.4/v/unexpected_keyword_argument
補足: ConfigDict
の extra
に指定できる文字列は以下の通りです。
ExtraValues = Literal['allow', 'ignore', 'forbid']
- https://docs.pydantic.dev/latest/api/config/#pydantic.config.ExtraValues
- https://docs.pydantic.dev/latest/concepts/config/
dataclass を使用する場合、allow
, ignore
どちらを extra に設定しても出力は以下の通りでした。
$ python example2.pyExample(key1='a', key2=1, key3=Key3(key4='c', key5='d'))
必要なデータが存在しない場合
先ほどはバリデーションするデータに余分なデータが入っていた場合の動作を確認してみました。
今度はバリデーションするデータが一部存在しなかった場合を確認してみます。
key4
と key5
を削除しました。
from pydantic.dataclasses import dataclass
@dataclassclass Key3: key4: str key5: str
@dataclassclass Example: key1: str key2: int key3: Key3
def from_example_api(): return { "key1": "a", "key2": 1, "key3": { "key4": "c", "key5": "d" }, }
if __name__ == "__main__": res = from_example_api() example = Example(**res)
print(example)
key4
, key5
が存在しない、とエラーが出ました。
$ python example3.pyTraceback (most recent call last): File "/Users/xxx/blog-code/2023/10/python-pydantic/example3.py", line 25, in <module> example = Example(**res) ^^^^^^^^^^^^^^ File "/Users/xxx/blog-code/2023/10/python-pydantic/venv/lib/python3.11/site-packages/pydantic/_internal/_dataclasses.py", line 132, in __init__ s.__pydantic_validator__.validate_python(ArgsKwargs(args, kwargs), self_instance=s)pydantic_core._pydantic_core.ValidationError: 2 validation errors for Examplekey3.key4 Field required [type=missing, input_value={}, input_type=dict] For further information visit https://errors.pydantic.dev/2.4/v/missingkey3.key5 Field required [type=missing, input_value={}, input_type=dict] For further information visit https://errors.pydantic.dev/2.4/v/missing
しかし、API のオプションなど取得の仕方によってはデータに key が存在する場合と、しない場合があると思います。
そのため、key の有無に関係なく定義した dataclass に変換してもらいたいです。
こんな場合は、str | None = None
といったように None との Optional にすることで、key の有無を必須にしなくても良くなります。
from pydantic.dataclasses import dataclass
@dataclassclass Key3: key4: str key4: str | None = None key5: str key5: str | None = None
@dataclassclass Example: key1: str key2: int key3: Key3
def from_example_api(): return { "key1": "a", "key2": 1, "key3": { }, }
if __name__ == "__main__": res = from_example_api() example = Example(**res)
print(example) print(example.key3.key4) print(example.key3.key5)
$ python example3.pyExample(key1='a', key2=1, key3=Key3(key4=None, key5=None))NoneNone
今回のようにネストしたデータ構造内の key の有無は dict との Optional + field_validator
でも対応できます。
ネストしたデータ内部の key が動的に変わる場合や Optional にしたい key が多い場合は、以下に紹介するやり方が便利かもしれません。
from pydantic.dataclasses import dataclassfrom pydantic import field_validator, ValidationError
@dataclassclass Key3: key4: str key5: str
@dataclassclass Example: key1: str key2: int key3: Key3 key3: Key3 | dict
@field_validator("key3") @classmethod def validate_key3(cls, v: dict) -> Key3 | dict: try: k = Key3(**v) except ValidationError: return v
return k
def from_example_api(): return { "key1": "a", "key2": 1, "key3": { }, }
if __name__ == "__main__": res = from_example_api() example = Example(**res)
print(example)
if isinstance(example.key3, Key3): print(example.key3.key4) print(example.key3.key5)
$ python example4.pyExample(key1='a', key2=1, key3={})
さいごに
今回は pydantic を使って学んだことをアウトプットしてみました。
pydantic のおかげで型安全になるので、例え簡単なスクリプトでも効果を発揮すると思います。
これから Python のコードを書くときは利用しようと思います。