pydanticを触ってみる
業務で AWS (boto3) から取得したデータをデータベースに格納するスクリプトを Python で書くタスクをしました。 その際 AWS から取得したデータをバリデーションして型安全にしたいと思い pydantic を導入してみたので備忘録を書きます。
今回使用したコードは以下に置いています。
https://github.com/kntks/blog-code/tree/main/2023/10/python-pydantic
pydantic とは
Section titled “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 にも対応しています。
基本的な使い方や説明はドキュメントを読んだ方が良いので、ここでは触れません。
モチベーション
Section titled “モチベーション”なぜ 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"))Noneget メソッドは第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"    }  }データクラスを使ったサンプル
Section titled “データクラスを使ったサンプル”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')エラーを出してみる
Section titled “エラーを出してみる”今度は 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余分なデータがある場合
Section titled “余分なデータがある場合”今度は 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'))必要なデータが存在しない場合
Section titled “必要なデータが存在しない場合”先ほどはバリデーションするデータに余分なデータが入っていた場合の動作を確認してみました。
今度はバリデーションするデータが一部存在しなかった場合を確認してみます。
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 のコードを書くときは利用しようと思います。