Skip to content

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 を使いました。

バージョン

バージョン
Python3.11.4
pydantic2.4.2

環境構築

Terminal window
$ python3 -m venv venv
$ source venv/bin/activate

必要なモジュールをインストールします。

Terminal window
$ pip install pydantic

検証

とある API からデータの取得を想定して、以下の関数を定義しています。

def from_example_api():
return {
"key1": "a",
"key2": 1,
"key3": {
"key4": "c",
"key5": "d"
}
}

データクラスを使ったサンプル

dataclass を使って簡単なデータをバリデーションしてみました。

example1.py
from pydantic.dataclasses import dataclass
@dataclass
class Key3:
key4: str
key5: str
@dataclass
class 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)

ここではとくに問題なさそうです。

Terminal window
$ python example1.py
a
1
Key3(key4='c', key5='d')

エラーを出してみる

今度は key2 を str 型にしてみます。本来 key2 の型は int なのでエラーが出るはずです。

example1.py
from pydantic.dataclasses import dataclass
@dataclass
class Key3:
key4: str
key5: str
@dataclass
class 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)

意図した通りにエラーが出ました。

Terminal window
$ python example1.py
Traceback (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 Example
key2
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 を追加してみました。

example2.py
from pydantic.dataclasses import dataclass
@dataclass
class Key3:
key4: str
key5: str
@dataclass
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)

実行してみると問題なく動きました。

Terminal window
$ python example2.py
Example(key1='a', key2=1, key3=Key3(key4='c', key5='d'))

必要のない key が存在した場合、エラーを出すには ConfigDictextra を設定する必要があります。

example2.py
from pydantic.dataclasses import dataclass
from pydantic import ConfigDict
config = ConfigDict(extra="forbid")
@dataclass
class 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 の箇所でエラーが出ました。

Terminal window
$ python example2.py
Traceback (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 Example
key6
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

補足: ConfigDictextra に指定できる文字列は以下の通りです。

ExtraValues = Literal['allow', 'ignore', 'forbid']

dataclass を使用する場合、allow, ignore どちらを extra に設定しても出力は以下の通りでした。

Terminal window
$ python example2.py
Example(key1='a', key2=1, key3=Key3(key4='c', key5='d'))

必要なデータが存在しない場合

先ほどはバリデーションするデータに余分なデータが入っていた場合の動作を確認してみました。
今度はバリデーションするデータが一部存在しなかった場合を確認してみます。

key4key5 を削除しました。

example3.py
from pydantic.dataclasses import dataclass
@dataclass
class Key3:
key4: str
key5: str
@dataclass
class 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 が存在しない、とエラーが出ました。

Terminal window
$ python example3.py
Traceback (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 Example
key3.key4
Field required [type=missing, input_value={}, input_type=dict]
For further information visit https://errors.pydantic.dev/2.4/v/missing
key3.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 の有無を必須にしなくても良くなります。

example3.py
from pydantic.dataclasses import dataclass
@dataclass
class Key3:
key4: str
key4: str | None = None
key5: str
key5: str | None = None
@dataclass
class 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)
Terminal window
$ python example3.py
Example(key1='a', key2=1, key3=Key3(key4=None, key5=None))
None
None

今回のようにネストしたデータ構造内の key の有無は dict との Optional + field_validator でも対応できます。

ネストしたデータ内部の key が動的に変わる場合や Optional にしたい key が多い場合は、以下に紹介するやり方が便利かもしれません。

from pydantic.dataclasses import dataclass
from pydantic import field_validator, ValidationError
@dataclass
class Key3:
key4: str
key5: str
@dataclass
class 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)
Terminal window
$ python example4.py
Example(key1='a', key2=1, key3={})

さいごに

今回は pydantic を使って学んだことをアウトプットしてみました。
pydantic のおかげで型安全になるので、例え簡単なスクリプトでも効果を発揮すると思います。
これから Python のコードを書くときは利用しようと思います。