🙌

pydantic で効率的かつモダンなデータシリアライズ・バリデーション

2021/01/28に公開
2

はじめに

この記事は、筆者がただただハマっている pydantic の良いところを紹介し続ける記事になっています。基本的には、「pydantic はいいものだ」という立場でお話をしています。他のライブラリのファンの人は、あまりいい気分がしないと思いますので、みないことをお勧めします。

https://meety.net/matches/FpfKplntqjli

pydantic

Python で書かれた Python 用のデータシリアライズ・バリデーションライブラリです。要するに、データをいい感じにオブジェクトにしてくれて、データに対しては型ヒントに基づく検証を実施してくれるツールです。

公式には以下のような記載があります。

Data validation and settings management using python type annotations. pydantic enforces type hints at runtime, and provides user friendly errors when data is invalid.

要するに、pydantic は以下の二点を満たしているところが良い点らしい。

  • 実行時に型情報を提供していること
  • データが不正な時に、ユーザーが理解しやすいエラーを出してくれること

pydantic の Pros&Cons

Pros

  • 実行時に意図しない型のデータが入力されてきた時にしっかりエラーを出してくれる
  • Swagger, JsonSchema, YAML などから自動でモデルを生成するライブラリがある
  • 類似ライブラリに比べて実行速度が早い
  • 実装済みのフィールドタイプが豊富かつ、mypy のプラグインがあり静的解析の導入が用意
  • FastAPIで使用されており、アプリケーション開発時に知見をそのまま流用できる

Cons

個人的には、ほとんどデメリットを感じていませんが強いていうなら...

  • 型をゆるふわに書いて、動的言語の良さを最大限生かしたい人には向かない。(Any タイプをたくさん使いたい人とか)
  • mypy や Typing に慣れていないと辛い

他の類似ツール

データのバリデーション・シリアライズでいうといくつか似たようなライブラリがあります。

他にも、 Protocol Buffers のスタブファイルなども実質的には似たような体験を提供してくれます。

APIフレームワークとの関係性

FastAPI では、モデルを定義する際に pydantic が使用されています。ちなみに、 ASGI サーバーは starlette

FastAPI は、Pythonの標準である型ヒントに基づいてPython 3.6 以降でAPI を構築するための、モダンで、高速(高パフォーマンス)な、Web フレームワークです。(公式より)

pydantic でモデルを定義して、APIサーバーを起動すると SwaggerUI が自動生成される便利な機能がついています。FastAPI についての説明は本記事では行いませんが、気になる方はこちらをどうぞ。個人的には、Flask よりも記述が簡単で、Flask よりも早いため、Python で小さいAPIサーバーを作るときは、まず第一選択肢にFastAPIを考えます。

コードジェネレーター

Swagger でいうところの swagger-py-codegen や、 Protocol Buffers でいうところの protoc コマンドのようなツールが pydantic にもあります。これがまた便利です。

datamodel-code-generator というツールになるんですが、 Json Schema や Swagger, YAML, JSON などのフォーマットを読み込んで、 pydantic ベースのモデルを自動生成してくれるツールになっています。

データ分析等の文脈で言えば、Json Schema などデータの構造が既知のデータを抽象的に扱いたい時に活躍します。例えば、自社の別のサービスを利用する際に、Swaggerなどがすでに公開されていれば、そこからモデルを自動作成して使用する、などです。

使い方は、 datamodel-codegen を pip 等でインストールし、以下のように使うだけです。

$ datamodel-codegen --input api.yaml --output model.py

このようなファイルが生成されます。詳しくは、こちら

# generated by datamodel-codegen:
#   filename:  api.yaml
#   timestamp: 2020-06-02T05:28:24+00:00

from __future__ import annotations

from typing import List, Optional

from pydantic import AnyUrl, BaseModel, Field


class Pet(BaseModel):
    id: int
    name: str
    tag: Optional[str] = None


class Pets(BaseModel):
    __root__: List[Pet]


class Error(BaseModel):
    code: int
    message: str


class Api(BaseModel):
    apiKey: Optional[str] = Field(
        None, description='To be used as a dataset parameter value'
    )
    apiVersionNumber: Optional[str] = Field(
        None, description='To be used as a version parameter value'
    )
    apiUrl: Optional[AnyUrl] = Field(
        None, description="The URL describing the dataset's fields"
    )
    apiDocumentationUrl: Optional[AnyUrl] = Field(
        None, description='A URL to the API console for each API'
    )


class Apis(BaseModel):
    __root__: List[Api]

pydantic を使ったコードの書き方

Settings managementより。シークレット情報を扱う際の機能も実装されています。こう見ると、 .proto ファイルにスキーマ情報を記述するのと、近い感じを受けませんか?

from typing import Set

from pydantic import (
    BaseModel,
    BaseSettings,
    PyObject,
    RedisDsn,
    PostgresDsn,
    Field,
)


class SubModel(BaseModel):
    foo = 'bar'
    apple = 1


class Settings(BaseSettings):
    auth_key: str
    api_key: str = Field(..., env='my_api_key')

    redis_dsn: RedisDsn = 'redis://user:pass@localhost:6379/1'
    pg_dsn: PostgresDsn = 'postgres://user:pass@localhost:5432/foobar'

    special_function: PyObject = 'math.cos'

    # to override domains:
    # export my_prefix_domains='["foo.com", "bar.com"]'
    domains: Set[str] = set()

    # to override more_settings:
    # export my_prefix_more_settings='{"foo": "x", "apple": 1}'
    more_settings: SubModel = SubModel()

    class Config:
        env_prefix = 'my_prefix_'  # defaults to no prefix, i.e. ""
        fields = {
            'auth_key': {
                'env': 'my_auth_key',
            },
            'redis_dsn': {
                'env': ['service_redis_dsn', 'redis_url']
            }
        }

実行時は、

>>> print(Settings().dict())
{
    'auth_key': 'xxx',
    'api_key': 'xxx',
    'redis_dsn': RedisDsn('redis://user:pass@localhost:6379/1',
scheme='redis', user='user', password='pass', host='localhost',
host_type='int_domain', port='6379', path='/1'),
    'pg_dsn': PostgresDsn('postgres://user:pass@localhost:5432/foobar',
scheme='postgres', user='user', password='pass', host='localhost',
host_type='int_domain', port='5432', path='/foobar'),
    'special_function': <built-in function cos>,
    'domains': set(),
    'more_settings': {'foo': 'bar', 'apple': 1},
}

参考

Discussion

korallekoralle

参考の3番目の記事を書きました。読んで頂きありがとうございます。

個人的には、ほとんどデメリットを感じていませんが強いていうなら...

型をゆるふわに書いて、動的言語の良さを最大限生かしたい人には向かない。(Any タイプをたくさん使いたい> 人とか)
mypy や Typing に慣れていないと辛い

実際に使ってみた感想として、デメリットについてはもう一つあると思っています。
pydanticはMicrosoft製のPythonのLanguage ServerであるPylanceとの相性が補完の面で若干悪いです
私が実際に遭遇した例はすぐに出せませんが、pydanticでちょっと複雑なことをやろうとすると補完で「あれ?」となることがありました。

これはpydanticがPython標準になったdataclasses.dataclassではなく独自の基底クラス(pydantic.BaseModel)に依存していることに起因しています。
pydanticをPylanceに完全に対応させるために色々考えられてはいますが、中々一筋縄では行かない様です。

その辺りの議論がPylanceのIssueでなされています。

とは言うものの、FastAPIで採用されていることもありPydantic自体は(特にバックエンドで)非常に役に立つ優秀なライブラリだと思います。

山本隼汰 | Hayata Yamamoto山本隼汰 | Hayata Yamamoto

参考の3番目の記事を書きました。読んで頂きありがとうございます。

いえいえ、こちらこそ!大変参考になりました。

pydanticはMicrosoft製のPythonのLanguage ServerであるPylanceとの相性が補完の面で若干悪いです。
私が実際に遭遇した例はすぐに出せませんが、pydanticでちょっと複雑なことをやろうとすると補完で「あれ?」となることがありました。

なるほど。これは認識してませんでした。開発の際にはちょっと苦労するかもしれませんね、確かに。
非常に有益な情報をありがとうございます。

とは言うものの、FastAPIで採用されていることもありPydantic自体は(特にバックエンドで)非常に役に立つ優秀なライブラリだと思います。

私も同感です。おそらく FastAPI 自体も Pythonで API 開発する際には一番最初に今後、選択されて来ると思いますし、 Pylance もきっと時期に対応してくれると信じたい...