Open2

Pydanticことはじめ

kun432kun432

Pythonで型を使ったコードは、せいぜい型ヒントぐらいで、恥ずかしながらPydanticをちゃんと使ったことがなかったので、やってみる。

kun432kun432

https://docs.pydantic.dev/latest/

インストール

Colaboratoryで。

まずColaboratoryデフォのPydanticのバージョンを確認。

!pip show pydantic

1.10.3と古い。Pydanticの最新は執筆時点で2.5.3。

Name: pydantic
Version: 1.10.13
Summary: Data validation and settings management using python type hints
(snip)

ということでアップグレード。

!pip install -U pydantic
!pip show pydantic
Name: pydantic
Version: 2.5.3
Summary: Data validation using Python type hints
(snip)

Getting Started

https://docs.pydantic.dev/latest/

PydanticのBaseModelを継承してDeliveryというデータモデルのクラスを定義する。
Deliveryクラスは、datetime型のtimestampと2つのIntを持つTuple型のdimensionsという属性を持たせている。

from datetime import datetime
from typing import Tuple

from pydantic import BaseModel

class Delivery(BaseModel):
    timestamp: datetime
    dimensions: Tuple[int, int]

ではDeliveryクラスのインスタンスを作成する。クラス定義のとおりに属性を指定してみる。

m = Delivery(timestamp=datetime.now(), dimensions=(10, 20))

でそれぞれの属性を見てみる。

print(repr(m.timestamp))
print(repr(m.dimensions))
datetime.datetime(2024, 1, 12, 13, 0, 11, 299111)
(10, 20)

ではもう一つインスタンスを作成して、少し属性の指定を変えてみる。

m2 = Delivery(timestamp='2024-01-12T21:45:05+0900', dimensions=['10.0', '20.0'])
print(repr(m2.timestamp))
print(repr(m2.dimensions))
datetime.datetime(2024, 1, 12, 21, 45, 5, tzinfo=TzInfo(+09:00))
(10, 20)

日付の文字列表現をdatetime型に変換してくれているし、文字列かつリストで渡した値もきちんとIntのTupleに変換してくれている。

なるほど、このように出力に型と制約を保証してくれるというものなのか。たしかに便利。

とはいえ、流石に無理なものは無理なので、こういうのは当然エラーになる。

m2 = Delivery(timestamp='2024-01-12T21:45:05+0900', dimensions=['あああ', '20.0'])
ValidationError: 1 validation error for Delivery
dimensions.0
  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='あああ', input_type=str]
m2 = Delivery(timestamp='2024年01月12日 21時45分00秒', dimensions=['10.0', '20.0'])
ValidationError: 1 validation error for Delivery
timestamp
  Input should be a valid datetime, invalid date separator, expected `-` [type=datetime_parsing, input_value='2024年01月12日 21時45分00秒', input_type=str]

Example

Userクラスを定義。

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]  

外部データをUserクラスの定義に適合させる。

external_data = {
    'id': 123,
    'signup_ts': '2019-06-01 12:22',  
    'tastes': {
        'wine': 9,
        b'cheese': 7,  
        'cabbage': '1',  
    },
}

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)  
print(user.model_dump())  

適合できるものは適合してくれている。

123
{'id': 123, 'name': 'John Doe', 'signup_ts': datetime.datetime(2019, 6, 1, 12, 22), 'tastes': {'wine': 9, 'cheese': 7, 'cabbage': 1}}

適合できない=バリデーションに失敗したらValidationErrorが例外で上がって、どこが悪いのかを教えてくれる。

from datetime import datetime
from pydantic import BaseModel, PositiveInt
from pydantic import ValidationError

class User(BaseModel):
    id: int  
    name: str = 'John Doe'  
    signup_ts: datetime | None  
    tastes: dict[str, PositiveInt]  


external_data = {'id': 'not a int', 'tastes': {'a': -1}}  

try:
    User(**external_data)
except ValidationError as e:
    print(e.errors())
[
    {
        'type': 'int_parsing',
        'loc': ('id',),
        'msg': 'Input should be a valid integer, unable to parse string as an integer',
        'input': 'not a int',
        'url': 'https://errors.pydantic.dev/2.5/v/int_parsing'
    },
    {
        'type': 'missing',
        'loc': ('signup_ts',),
        'msg': 'Field required',
        'input': {
            'id': 'not a int',
            'tastes': {'a': -1}
        },
        'url': 'https://errors.pydantic.dev/2.5/v/missing'
    },
    {
        'type': 'greater_than',
        'loc': ('tastes', 'a'),
        'msg': 'Input should be greater than 0',
        'input': -1,
        'ctx': {'gt': 0},
        'url': 'https://errors.pydantic.dev/2.5/v/greater_than'
    }
]