pydantic入門
Pydanticとはなにか
Data validation and settings management using Python type annotations.
pydantic enforces type hints at runtime, and provides user friendly errors when data is invalid.
Define how data should be in pure, canonical Python; validate it with pydantic.
データのバリデーションや型注釈の設定に使われるモジュール。
なにがメリットなの?
ランタイムでの型ヒントを強制してくれること。
dataclassでは強制をしてくれないので事故が起きうる(型安全でない)
Pydanticを使う
セットアップ
$ pip install pydantic
モデルの定義
JSON
バリデーションとテスト
Models:公式
BaseModel
を継承させて使う
パースであってバリデーションじゃない...?
pydantic is primarily a parsing library, not a validation library. In other words, pydantic guarantees the types and constraints of the output model, not the input data
パースのためのライブラリであってバリデーションが目的のライブラリじゃないんです...?
出力データは保証するけど入力データはしないよ...?違いが難解(esoteric distinction)すぎる...と思う...が
=> Models - pydantic
from pydantic import BaseModel
class User(BaseModel):
id: int
name = 'Jane Doe'
if __name__ == "__main__":
user = User(id='123')
# pydantic.error_wrappers.ValidationError: 1 validation error for User id value is not a valid integer (type=type_error.integer)
user_x = User(id='123.45')
Data Conversion
pydantic may cast input data to force it to conform to model field types, and in some cases this may result in a loss of information.
Modelのプロパティ
-
dict
modelのフィールドと値をdictで返す -
json
modelのフィールドと値をjsonで返す -
copy
modelのコピー(デフォルトだとshallow copy)を返す -
parse_obj() TODO
任意のオブジェクトをモデルにロードする -
parse_raw() TODO
様々な形式の文字列を読み込む -
parse_file()
like parse_raw() but for file paths -
from_orm()
loads data into a model from an arbitrary class -
schema()
returns a dictionary representing the model as JSON Schema -
construct()
-
__fields_set__
-
__fields__
-
__config__
Recursive Models
More complex hierarchical data structures can be defined using models themselves as types in annotations.
モデル自体をアノテーションの型として使える
from typing import List, Optional
from pydantic import BaseModel
class Foo(BaseModel):
count: int
size: Optional[float] = None
class Bar(BaseModel):
apple = 'x'
banana = 'y'
class Spam(BaseModel):
foo: Foo
bars: List[Bar]
ORM Mode(任意のクラスインスタンス?)
Pydantic models can be created from arbitrary class instances to support models that map to ORM objects.
- コンフィグの
orm_mode
はTrue
に - モデルインスタンスを作るために
from_orm
というコンストラクタを使用すること
from sqlalchemy import Column, Integer, String
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.ext.declarative import declarative_base
from pydantic import BaseModel, constr
Base = declarative_base()
class CompanyOrm(Base):
__tablename__ = 'companies'
id = Column(Integer, primary_key=True, nullable=False)
public_key = Column(String(20), index=True, nullable=False, unique=True)
name = Column(String(63), unique=True)
domains = Column(ARRAY(String(255)))
class CompanyModel(BaseModel):
id: int
public_key: constr(max_length=20)
name: constr(max_length=63)
domains: list[constr(max_length=255)]
class Config:
orm_mode = True
co_orm = CompanyOrm(
id=123,
public_key='foobar',
name='Testing',
domains=['example.com', 'foobar.com'],
)
print(co_orm)
#> <models_orm_mode_3_9.CompanyOrm object at 0x7fcc98a0f760>
co_model = CompanyModel.from_orm(co_orm)
print(co_model)
#> id=123 public_key='foobar' name='Testing' domains=['example.com',
#> 'foobar.com']
from pydantic import BaseModel
from dataclasses import dataclass
@dataclass
class InventoryItem:
name: str
quantity_on_hand: int = 0
class User(BaseModel):
id: int
name = "Jane Doe"
class UserF(BaseModel):
f_id: float
name = "Kumamot"
if __name__ == "__main__":
user_a = User(id=123)
user_b = User(id="123")
user_c = User(id=123.45)
user_d = User(id=123, name="Kagoshima")
print("user_d.dict()", user_d.json()) # debug
# pydantic.error_wrappers.ValidationError: 1 validation error for User id value is not a valid integer (type=type_error.integer)
# user_x = User(id='123.45') #これはアウト
# データのコンバージョンが行われていることに注目!!!
assert user_a.id == 123
assert user_b.id == 123
assert user_c.id == 123
# デフォルト値は__fields_set__に現れない
assert user_a.__fields_set__ == {"id"}
assert user_d.__fields_set__ == {"id", "name"}
# ----------------------------
user_1 = UserF(f_id=1.2)
user_2 = UserF(f_id=1)
user_3 = UserF(f_id="3.1")
user_3 = UserF(f_id="3")
user_3.f_id
Reserved Name これは飛ばす
SQLAlchemy で予約されたフィールドの名前のカラムの名前にしたい場合
Recursive ORM models
ORM instances will be parsed with from_orm recursively as well as at the top level.
再帰的にいけるよーって話。べんり
Error Handling
e.errors()
e.json()
str(e)
Custom Errors これはvalidationのところで再度
In your custom data types or validators you should use ValueError, TypeError or AssertionError to raise errors.
Helper Functions
Pydantic provides three classmethod helper functions on models for parsing data:
-
parse_obj
キーワード引数でなくdictで受け取るというところを除けば概ね__init__
みたいな働きをするってよ。
dict以外を受け取るとValidationError
!!って怒る。 -
parse_raw
strやbytesを受け取ってそれをjsonにパースする。さらにその結果をparse_objに渡してくれる。めっちゃ気が利くやつだなぁ...
content_type引数を適切に設定しておけばpickleデータもサポートしてくれるらしい。やべぇな。
- parse_file
ファイルパスを受け取り=>parse_rawに。
content_type is omitted, it is inferred from the file's extension.
Creating models without validation
construct()
を与えるとvalidationなしのモデルを作ることが出来る。
インプットのデータが十分にvalidateされている保証がある時に用いる
original_user = User(id=123, age=32)
fields_set = original_user.__fields_set__
// validationなし、ちょっと早い
new_user = User.construct(_fields_set=fields_set, **user_data)
Generic Models TODO:あとでもっと読む
Pydantic supports the creation of generic models to make it easier to reuse a common model structure.
In order to declare a generic model, you perform the following steps:
-
モデルのパラメーター化のために1つ以上の
typing.TypeVar
インスタンスを宣言する -
pydantic.generics.GenericModel
とtyping.Generic
を継承したpydanticモデルを宣言する。その際にtyping.GenericにはパラメーターとしてTypeVar
インスタンスを渡す -
入れ替えたいタイプやpydanticモデルの代わりに
TypeVar
インスタンスをアノテーションとして用いる
from typing import Generic, TypeVar
from pydantic.generics import GenericModel
DataT = TypeVar('DataT')
class Response(GenericModel, Generic[DataT]):
data: DataT | None
error: Error | None
To inherit from a GenericModel without replacing the TypeVar instance, a class must also inherit from typing.Generic
from typing import TypeVar, Generic
from pydantic.generics import GenericModel
TypeX = TypeVar('TypeX')
class BaseClass(GenericModel, Generic[TypeX]):
X: TypeX
class ChildClass(BaseClass[TypeX], Generic[TypeX]):
# Inherit from Generic[TypeX]
pass
# Replace TypeX by int
print(ChildClass[int](X=1))
#> X=1
下記のように一部だけ書き換えも可能
class BaseClass(GenericModel, Generic[TypeX, TypeY]):
x: TypeX
y: TypeY
class ChildClass(BaseClass[int, TypeY], Generic[TypeY, TypeZ]):
z: TypeZ
# Replace TypeY by str
print(ChildClass[str, int](x=1, y='y', z=3))
#> x=1 y='y' z=3
Dynamic model creation
モデルの形状が実行時までわからない場合に備えたメソッドであるcreate_model
Modelの話ここまで
Dataclass
標準的な pydantic のフィールド型はすべて使用でき、 結果のデータクラスは標準ライブラリ dataclass デコレータで作成したものと同じに。
基礎となるモデルとそのスキーマは__pydantic_model__ でアクセスできる。
pydantic.dataclasses.dataclass's arguments are the same as the standard decorator, except one extra keyword argument config
config って引数だけdataclassより増えてる。これはBaseModelで使えるとやつと一緒。
from pydantic import ConfigDict
from pydantic.dataclasses import dataclass
# Option 1 - use directly a dict
# Note: `mypy` will still raise typo error
@dataclass(config=dict(validate_assignment=True))
class MyDataclass1:
a: int
# Option 2 - use `ConfigDict`
# (same as before at runtime since it's a `TypedDict` but with intellisense)
@dataclass(config=ConfigDict(validate_assignment=True))
class MyDataclass2:
a: int
# Option 3 - use a `Config` class like for a `BaseModel`
class Config:
validate_assignment = True
@dataclass(config=Config)
class MyDataclass3:
a: int
Convert stdlib dataclasses into pydantic dataclasses
__post_init_post_parse__
の助けを借りて、バリデーション後にコードを実行することが可能である。これは、検証前にコードを実行する __post_init__
と同じではありません。
If you use a stdlib dataclass, you may only have post_init available and wish the validation to be done before. In this case you can set Config.post_init_call = 'after_validation'
Difference with stdlib dataclasses
dataclasses.dataclass を pydantic.dataclasses.dataclass で代用する場合、 post_init メソッドで実行されるコードを post_init_post_parse メソッドに移動し、検証前に実行する必要があるコードの一部のみを 残しておくことが推奨されます。
JSON Dumping
Pydantic dataclasses do not feature a .json() function. To dump them as JSON, you will need to make use of the pydantic_encoder as follows:
Pydantic データクラスは .json() 関数を備えていません。それらをJSONとしてダンプするには、以下のようにpydantic_encoderを利用する必要があります。
pydanticのdataclass利用でもっと安全なPythonコード実現する
Lightweight LanguageであるPythonの良さの1つに「気楽に書ける」ということが挙げられると思いますが、そのカジュアルな書き味を多少捨ててでもtypeヒントやdataclass等を用いたより"硬い"コードの方が読む際の認知負荷が減って良いなぁ、と最近は思うようになってきました。
この記事では既にそのような"硬い"Pythonコードを書くためにdataclassを用いている人に向けて代わりに pydantic を使うという選択もアリだヨ!というお話をしていきたいと思います。[1]
pydanticは実行時に型ヒントを強制し、データのvalidation結果が無効な場合は分かりやすいエラーを提供してくれるPythonライブラリです。pipで簡単にインストール出来ます。
$ pip install pydantic
この記事を読むことで「dataclassのツラミがどこにありpydanticはどうそれを解決してくれるのか」について知ることが出来ます。またdataclassのツラミの解消だけでなく「pydanticの提供してくれるオリジナルの型がいかに便利か」や「pydanticで実装するカスタムバリデーションがいかに痒いところに手が届く仕様になっているのか」について学びを得られるように説明していきます。
※もし「ここの説明は違くないか?」等あればコメント等で指摘いただけると幸いです。
1. dataclassの辛みを癒やすpydantic 🐶
以前、Zenn記事:PEP557から読み解くPythonのdataclassの嬉しさと他手段との比較でも書きましたが、dataclassを使っていて若干のツラミが2つあるなと感じています。
1-1. 型安全性
dataclassの一番のツラミは型注釈を全く無視したコードがランタイムでエラーにならないことだと思います。
dataclassではmypyでエラーになったコードもランタイムではシレッと実行されてしまう😓
from dataclasses import dataclass
@dataclass
class Company:
corporate_id: int
name: str
country_id: int
good_company = Company(corporate_id=1, name="熊本株式会社", country_id=81)
print(good_company.corporate_id, good_company.name) # debug
# mypyでエラーになるのでランタイムでもエラーになって欲しいが,シレっと実行できてしまう
bad_company1 = Company(corporate_id="not int", name="鹿児島株式会社", country_id=81)
print(bad_company1.corporate_id, bad_company1.name, bad_company1.country_id) # debug
pydanticを用いれば
pydanticならランタイムでエラーにちゃんとなってくれる😆
# from dataclasses import dataclass
from pydantic.dataclasses import dataclass
@dataclass
class Company:
corporate_id: int
name: str
country_id: int
good_company = Company(corporate_id=1, name="熊本株式会社", country_id=81)
print(good_company.corporate_id, good_company.name, good_company.country_id) # debug
# pydantic.error_wrappers.ValidationError: 1 validation error for Company corporate_id value is not a valid integer (type=type_error.integer)
bad_company1 = Company(corporate_id="not int", name="鹿児島株式会社", country_id=81)
ただしpydanticには若干癖があり注意が必要なポイントがあります。
pydantic is primarily a parsing library, not a validation library. Validation is a means to an end: building a model which conforms to the types and constraints provided.
In other words, pydantic guarantees the types and constraints of the output model, not the input data.
pydanticは 入力データのvalidationのライブラリではなく、出力データの型の保証をするparseのライブラリである ということです。
入力データがcast出来るならエラーにならない。出力データの保証に力点がある😳
from pydantic.dataclasses import dataclass
@dataclass
class Company:
corporate_id: int
name: str
country_id: int
# 勝手にcast出来るものはcastされる
bad_company2 = Company(corporate_id=2.1, name=3.14, country_id="2")
print(bad_company2.corporate_id, type(bad_company2.corporate_id)) # 2 <class 'int'>
print(bad_company2.name, type(bad_company2.name)) # 3.14 <class 'str'>
print(bad_company2.country_id, type(bad_company2.country_id)) # 2 <class 'int'>
1-2. JSON化
dictやjson形式でのデータしか受け入れてくれないプログラムにデータを送らなくてはいけない機会は多々ありますが、 標準のdataclassにはdict/jsonへの備え付けの変換機能はありません。
dataclasses-jsonという別ライブラリを用いるのも手ですが、 pydanticならpydantic.json.pydantic_encoder
と標準ライブラリのjsonと併用でJSON化出来ます。
標準ライブラリと併用で簡単にJSON化出来る😆
import dataclasses
import json
from pydantic.dataclasses import dataclass
from pydantic.json import pydantic_encoder
@dataclass
class User:
id: int
name: str = "John Doe"
friends: list[int] = dataclasses.field(default_factory=lambda: [0])
user = User(id="42")
# JSON化
user_json = json.dumps(user, indent=4, default=pydantic_encoder)
(これはpydanticと直接は関係ない話ですが)当然JSON化出来ればdictにも変換は容易です。
# JSONからdictへは当然変換可能😆
user_dict = json.loads(user_json)
2. オリジナルの便利な型を提供してくれるpydantic 🤖
pydantic提供のオリジナル型は非常に多彩で便利なものです。
ここではその一部を紹介します。
他のオリジナル型についてはField Types - pydanticを参考にしてみてください。
型 | 制限 | 補足 |
---|---|---|
FutureDate | 未来の日付 | - |
PastDate | 過去の日付 | - |
EmailStr | emailアドレス準拠の文字列 | 追加でemail-validator ライブラリが別途必要 |
HttpUrl | httpかhttpsで始まるURL | - |
PositiveInt | 正の整数 | - |
conint | int型の範囲を指定 |
gt で「より大きい」ge で「以上」の意味。 |
confloat | float型の範囲を指定 | - |
SecretStr | 機密にしたい文字列、表示の際隠し文字になる | - |
pydantic提供のオリジナル型を使ってみる(`email-validator`ライブラリが別途必要)😆
from datetime import datetime, timedelta
from pydantic import (
EmailStr,
FutureDate,
HttpUrl,
PastDate,
PositiveFloat,
PositiveInt,
confloat,
conint,
SecretStr,
)
from pydantic.dataclasses import dataclass
@dataclass
class User:
user_id: int
auth_date: PastDate
expire_date: FutureDate
email_addres: EmailStr
user_page: HttpUrl
age: PositiveInt
height: PositiveFloat
money: conint(gt=1000, lt=1024)
score: confloat(ge=3.5, lt=5)
password: SecretStr
user = User(
user_id=1,
auth_date=datetime.now() - timedelta(days=1),
expire_date=datetime.now() + timedelta(days=1),
email_addres="hoge@example.com",
user_page="https://zenn.dev/",
age=12,
height=1.72,
money=1001,
score=3.5,
password="hogehoge",
)
3. 痒い所に手が届くカスタムバリデーションが簡単に実装できるpydantic 🚨
pydanticの@validator
デコレータを使えばフィールドに対するカスタムしたバリデーションを簡単に実装することが出来ます。
デコレータで簡単にバリデーションを付けられる😆
from pydantic import validator
from pydantic.dataclasses import dataclass
@dataclass
class User:
name: str
nick_name: str
password1: str
password2: str
@validator("name")
def name_must_contain_space(cls, v):
if " " not in v:
raise ValueError("must contain a space")
return v.title()
@validator("password2")
def passwords_match(cls, v, values, **kwargs):
# vとvaluesにはそれぞれフィールド値と該当のフィールド以外のデータが入ったdictがある
print("v", v) # hogehoge
print("values", values) # {'name': 'くまもと はまち', 'nick_name': 'kagoshima', 'password1': 'hogehoge'}
if "password1" in values and v != values["password1"]:
raise ValueError("passwords do not match")
return v
@validator("nick_name")
def username_alphanumeric(cls, v):
assert v.isalnum(), "must be alphanumeric"
return v
user = User(
name="くまもと はまち",
nick_name="kagoshima",
password1="hogehoge",
password2="hogehoge"
)
上記のコード内のバリデーションメソッドの第二引数のv
はフィールドに与えられた値で、第三引数のvalues
は該当のフィールド以外のフィールドのフィールド名と値のdictが入っています。
※いずれも引数の変数名も自由に変えることが出来ます。
またバリデーションを複数ヶ所で再利用するコードもvalidator
のallow_reuse
引数にTrue
を渡すだけで簡単実装で済みます。
バリデーションの再利用が容易😆
from pydantic import validator
from pydantic.dataclasses import dataclass
def name_must_contain_space(cls, v):
if " " not in v:
raise ValueError("must contain a space")
return v.title()
@dataclass
class Person1:
name: str
# validators
_normalize_name = validator("name", allow_reuse=True)(name_must_contain_space)
@dataclass
class Person2:
name: str
# validators
_normalize_name = validator("name", allow_reuse=True)(name_must_contain_space)
kagoshima = Person1(name="kagoshima gyoko")
print("kagoshima.name", kagoshima.name) # debug
kumamoto = Person2(name="kumamoto hamachi")
print("kumamoto.name", kumamoto.name) # debug
その他、validator
の引数をpre=True
とすることで他のValidator より優先的にバリデーションを行ったり、always=True
とすることでパフォーマンス問題でデフォルト値のあるフィールドに値が与えられなかった際にもバリデーションを行うなど細やかな設定をすることが出来ます。
まとめと参考 📗
pydanticを用いるとより堅牢なコードを容易に作ることが出来る、ということを上手く本記事が伝えることができていれば幸いです。
pydanticには他にも様々な機能があり今回紹介したdataclassの代替として用いるよりもむしろModelを使ったやり方の方が主要なものとなります。自分自身pydanticを利用したFast API、Django Ninja等のFWについて学びも深めてより踏み込んだpydanticの使い方について次回以降アウトプット出来るように精進していこうと思います!
以下、今回参考にさせていただいた参考資料ですm(_ _)m
-
今回の記事では既にFast APIやDjango Ninja等のFWでpydanticを使っている方向けではない記事にしたので敢えてdataclassの代用としての側面に焦点を当てた構成としていますm(_ _)m ↩︎
the keyword argument pre will cause the validator to be called prior to other validation
記事にした。オワリ。