pydanticを使ってpythonで型のチェックをする

Nov 18, 2023 22:40 · 1675 words · 4 minute read python

とあるツールのコードを見ていたらpydanticというライブラリを使っていました。 調べてみるととても便利そうだったので、学んだことをブログにアウトプットします。

目次

pydanticでできること

pydanticは、Python実行時に型のチェックをしてくれます。 データの型を指定することで、動的型付け言語であるPythonでも型チェックの恩恵を受けることができます。

dataclassと似ていますが、dataclassは期待する型を示すのみであり、実際のデータとして違う型が与えられてもエラーは発生しません。 pydanticでは型が違う場合にエラーで検知したり、組み込みのvalidatorで値をチェックできます。

公式ドキュメント

Welcome to Pydantic - Pydantic

インストール

$ pip install pydantic

email 用のvaridatorもあるそうです。下記のいずれかのコマンドでインストールします。

$ pip install pydantic[email]
$ pip install email-validator

Installation - Pydantic

サンプルコード

from pydantic import BaseModel
from typing import List
from datetime import datetime


class Item(BaseModel):
    id: int
    name: str
    release_date: datetime = None
    category: List[int] = []


item_dict = {
    'id': 1,
    'name': "apple",
    'release_date': datetime.now(),
    'category': [1, 2, 3]
}

# parse.object()はpydanticv2で非推奨となった
# item = Item.parse_obj(item_dict)
item = Item(**item_dict)

print(item.id)
print(item.name)
print(item.release_date)
print(item.category)

idをaにすると、エラーが発生します。

pydantic_core._pydantic_core.ValidationError: 1 validation error for Item id Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value=‘a’, input_type=str]

idを「’id’: ‘2’」とした場合は、値をよしなにintに変換してくれます。

item_dict = {
    'id': '2',
    'name': "apple",
    'release_date': datetime.now(),
    'category': [1, 2, 3]
}

item = Item(**item_dict)

print(item.id)

関数の引数や戻り値の型として指定することもできます。 コードが見やすくなりますね。

from pydantic import BaseModel
from typing import List
from datetime import datetime


class Item(BaseModel):
    id: int
    name: str
    release_date: datetime = None
    category: List[int] = []


item_dict = {
    'id': 1,
    'name': "apple",
    'release_date': datetime.now(),
    'category': [1, 2, 3]
}


class Report(BaseModel):
    items: List[Item] = []


def item_check(item: Item) -> Report:
    return Report(items=[item])


item = Item(**item_dict, strict=True)
report = item_check(item)
print(report)

ただ、関数から値を返す場合はpydanticのvalidatorが実行されるわけではないので、違う値を返したとしてもエラーにはなりません。 このあたりをチェックする場合はmypyを使うのが良さそうです。

def item_check(item: Item) -> Report:
    #return Report(items=[item])
    return True

mypyの場合に出力されるエラー

error: Incompatible return value type (got "bool", expected "Report")  [return-value]

validator

field_validator(), model_validator()を使って値のチェックができます。

pydanticのv1ではvaridator()とroot_validator()でしたが、これらの関数はv2で非推奨になりました。

from pydantic import BaseModel, field_validator, model_validator


supported_items: set = {'apple', 'orange', 'banana'}


class Item(BaseModel):
    name: str
    high_price: int
    low_price: int

    @field_validator('name')
    def check_name(cls, v):
        if v not in supported_items:
            raise ValueError(f'The input for name {v} is not supported.')
        return v

    @field_validator('high_price')
    def check_hight_price(cls, v):
        if v > 100:
            raise ValueError('The input for high_price should be greater than 100.')
        return v
   
    @model_validator(mode='before')
    def check_low_high(cls, values):
        if values['low_price'] > values['high_price']:
            raise ValueError('The input for low_price should be less than high_price.')
        return values


item_dict = {
    'name': 'apple',
    'high_price': 100,
    'low_price': 50
}

item = Item(**item_dict)
print(item)

jsonデータの読み書き

まずはjsonの書き出しから。 model_dump_json()を使うことでpydanticのmodelをjsonとして出力できます。 json()は非推奨になり、model_dump_json()の使用が推奨されています。

from pydantic import BaseModel, field_validator, model_validator
from pathlib import Path


supported_items: set = {'apple', 'orange', 'banana'}


class Item(BaseModel):
    name: str
    high_price: int
    low_price: int

    @field_validator('name')
    def check_name(cls, v):
        if v not in supported_items:
            raise ValueError(f'The input for name {v} is not supported.')
        return v

    @field_validator('high_price')
    def check_hight_price(cls, v):
        if v > 100:
            raise ValueError('The input for high_price should be greater than 100.')
        return v
   
    @model_validator(mode='before')
    def check_low_high(cls, values):
        if values['low_price'] > values['high_price']:
            raise ValueError('The input for low_price should be less than high_price.')
        return values


item_dict = {
    'name': 'apple',
    'high_price': 100,
    'low_price': 50
}

item = Item(**item_dict)
fpath = Path('sample.json')
fpath.write_text(item.model_dump_json())

続いてjsonの読み込み。 parse_file()はpydantic v2で非推奨になったので、将来的に使えなくなります。 代わりにmodel_validate_json()を使うことが推奨されていますが、この場合はファイルのデータを一度jsonライブラリで取り出す必要があります。 今まではparse_file()一発で読み込めていたので、ひと手間増えるのは少し残念ですね。

Migration Guide - Pydantic

from pydantic import BaseModel, field_validator, model_validator
from pathlib import Path


supported_items: set = {'apple', 'orange', 'banana'}


class Item(BaseModel):
    name: str
    high_price: int
    low_price: int

    @field_validator('name')
    def check_name(cls, v):
        if v not in supported_items:
            raise ValueError(f'The input for name {v} is not supported.')
        return v

    @field_validator('high_price')
    def check_hight_price(cls, v):
        if v > 100:
            raise ValueError('The input for high_price should be greater than 100.')
        return v
   
    @model_validator(mode='before')
    def check_low_high(cls, values):
        if values['low_price'] > values['high_price']:
            raise ValueError('The input for low_price should be less than high_price.')
        return values


fpath = Path('sample.json')
item = Item.parse_file(fpath)
print(item)

最後に

pydanticについて調べ、サンプルコードを書いて動かしてみました。 とても便利だと感じたので、今後の個人開発でも使おうと思います。 今回は試していませんが、SQLAlchemyとも相性が良さそうなので、こちらも試してみたいですね。

tweet Share