python

【Python】Pydanticで始めるPythonのデータバリデーション

AIエージェントの検証の一環として、langgraphでPydanticを使用していましたので、今回は、Pydanticについてまとめていきます。

Pydanticは、Pythonの型ヒントを活用して、データのバリデーションや型変換を自動化する強力なライブラリです。
これにより、コードの信頼性と可読性が向上し、開発効率が飛躍的に高まります。

本記事では、Pydanticの基本的な使い方から、FastAPIとの統合によるAPI開発、環境変数の管理と設定の読み込みなど、実践的な応用方法まで解説します。

それでは、早速見ていきましょう。

Pydanticの概要

Pydanticとはどのようなライブラリか

Pydanticは、Pythonでデータのバリデーションや型の強制変換を簡単かつ効率的に行うためのライブラリになります。
具体的には、以下のような際に使用します。

  • 入力データが正しい型や形式かどうかを簡単にチェックできる
  • データの型を自動的に適切な形式に変換できる
  • データの構造をわかりやすく定義できる

Pydanticは、Pythonの型ヒントを最大限に活用することで、コードの可読性や安全性を高めつつ、開発者が意図した通りのデータを扱えるようにしてくれます。

Pydanticの主な特徴と利点

Pydanticの特徴と利点は以下の通りです。

  • 型のバリデーションと型変換の自動化を実施
  • シンプルで直感的なモデル定義が可能
  • エラーメッセージが詳細でわかりやすい
  • FastAPIとの高い親和性
  • カスタマイズ性の高さ

それでは、Pydanticの具体的な使い方や応用例について詳しく解説していきます。

Pydanticの基本的な使い方

Pydanticのインストール方法

Pydanticを使い始めるには、まずPython環境にPydanticをインストールする必要があります。

pip install pydantic

インストールが完了したら、以下のようにインポートして使う準備が整います。

from pydantic import BaseModel

基本的なモデルの定義方法

Pydanticでは、データ構造を表すために「モデル」を作成します。
以下は基本的なモデル定義の例です。

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str
    email: str

このコードでは、Userというモデルを定義しています。
このモデルには次の3つの属性があります。

  • id:int型
  • name:str型
  • email:str型

Pydanticのモデルを作る際には、BaseModelを継承する必要があります。
定義したモデルは、次のようにインスタンス化して使用できます。

user = User(id=1, name="山田太郎", email="yamada_taro@example.com")
print(user)

以下のように出力されます。

出力: id=1 name='山田太郎' email='yamada_taro@example.com'

データのバリデーションと型変換の自動化

Pydanticの大きな特徴は、データのバリデーションと型変換を自動で行ってくれる点です。
モデルをインスタンス化する際に、入力データが定義した型と一致していない場合、適切に変換を試みたり、エラーを報告してくれます。

user = User(id="1", name="山田太郎", email="yamada_taro@example.com")
print(user)

この場合、idに文字列("1")を渡していますが、Pydanticが自動的に整数(1)に変換してくれます。

 出力: id=1 name='山田太郎' email='yamada_taro@example.com'

一方で、変換できないデータが渡された場合はエラーを発生させます。

user = User(id="山田花子", name="山田太郎", email="yamada_taro@example.com")

エラーメッセージから、どのフィールドがどのように間違っているのかも簡単に理解できます。

出力:
pydantic_core._pydantic_core.ValidationError: 1 validation error for User
id
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='山田花子', input_type=str]

このように、Pydanticの自動型変換とバリデーション機能を活用することでデータ処理の信頼性を向上させることができます。

高度な機能

ネストされたモデルの定義

Pydanticでは、モデルの中に別のモデルを含む「ネストされたモデル」を簡単に定義することができます。

以下は、ユーザー情報に住所情報を含むモデルを定義する場合になります。

from pydantic import BaseModel

class Address(BaseModel):
    street: str
    city: str
    zipcode: str

class User(BaseModel):
    id: int
    name: str
    email: str
    address: Address

address_data = {"street": "123 Main Street", "city": "Tokyo", "zipcode": "100-1111"}
user_data = {"id": 1, "name": "山田太郎", "email": "yamada_taro@example.com", "address": address_data}

user = User(**user_data)
print(user)

以下、出力結果です。

出力: 
id=1 name='山田太郎' email='yamada_taro@example.com' address=Address(street='123 Main Street', city='Tokyo', zipcode='100-1111')

このように、AddressモデルをUserモデルの中で利用することで、ユーザーの住所情報を含むデータを表現できます。
ネストされたモデルを使うと、複数のデータ構造を簡潔かつ明確に管理できるため、可読性が向上します。

カスタムバリデーションの実装方法

Pydanticでは、独自のバリデーションロジックを実装するためにカスタムバリデーションを追加することができます。
これには、@field_validatorデコレータを使用します。

例えば、ユーザーの名前が3文字以上でなければならないというルールを追加したい場合は以下のようにします。

from pydantic import BaseModel, field_validator

class User(BaseModel):
    id: int
    name: str
    email: str

    @field_validator('name')
    def name_must_be_long_enough(cls, value: str) -> str:
        if len(value) < 3:
            raise ValueError("名前は3文字以上である必要があります。")
        return value

# バリデーションエラー例
invalid_user = User(id=2, name="山田", email="yamada_hazime@example.com")

以下、出力結果です。

出力:
Value error, 名前は3文字以上である必要があります。 [type=value_error, input_value='山田', input_type=str]

カスタムバリデーションを利用することで、特定のビジネスルールに応じたデータチェックを簡単に追加できます。

デフォルト値とOptional型の活用

Pydanticのモデルでは、属性にデフォルト値を設定することができます。
デフォルト値を持つ属性は、データが渡されなかった場合に自動的にその値が使われます。

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str
    email: str
    is_active: bool = True

user = User(id=1, name="山田太郎", email="yamada_taro@example.com")
print(user)

以下、出力結果です。

出力: id=1 name='山田太郎' email='yamada_taro@example.com' is_active=True

また、属性が必須ではない場合には、Optional型を使用します。
Optional型を使うことで、データが渡されなくてもエラーにならないようにできます。

from typing import Optional
from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str
    email: Optional[str] = None  # メールアドレスは必須ではない

user = User(id=1, name="山田太郎" )
print(user)

以下、出力結果です。

出力: id=1 name='山田太郎' email=None

Optional型とデフォルト値を組み合わせることで、柔軟なモデルを定義できます。
これらの機能は、データのすべての項目が必須ではない場合や、デフォルト設定を適用したい場合に使用します。

Pydanticの実践的な応用

FastAPIとの統合によるAPI開発

FastAPIは、Pythonで高速なAPIを構築するためのフレームワークであり、Pydanticと深く統合されています。

ユーザー情報を受け取るエンドポイントを作成する場合、以下のようにPydanticのBaseModelを継承したクラスを定義できます。

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    id: int
    name: str
    email: str

@app.post("/users/")
async def create_user(user: User):
    return user

このコードでは、Userモデルがリクエストボディとして受け取るデータの構造と型を定義しています。
FastAPIはこのモデルを利用して、受信したデータが期待された型と一致するかを自動的に検証します。

LanggraphにおいてPydanticモデルを用いたステート管理

LangGraphは、複雑なAIエージェントを視覚的に管理し、ノードとエッジを使って直感的に処理フローを設計できるツールです。
LangGraphについては、以下を参考にしてください。

このLangGraphで、Pydanticのモデルをステート管理に活用することで、データのバリデーションや型安全性を高めることができます。

例えば、以下のようにPydanticモデルを定義し、LangGraphで使用します。

from pydantic import BaseModel
from langgraph.graph import StateGraph, START, END

# Pydanticモデルの定義
class UserState(BaseModel):
    name: str
    age: int
    is_adult: bool = False

# ノード関数の定義
def check_age(state: UserState):
    if state.age >= 18:
        state.is_adult = True
    return state

def greet_user(state: UserState):
    if state.is_adult:
        print(f"Hello, {state.name}. You are an adult.")
    else:
        print(f"Hi, {state.name}. You are not an adult yet.")
    return state

# グラフの構築
builder = StateGraph(UserState)
builder.add_node(check_age)
builder.add_node(greet_user)
builder.add_edge(START, "check_age")
builder.add_edge("check_age", "greet_user")
builder.add_edge("greet_user", END)
graph = builder.compile()

# グラフの実行
initial_state = UserState(name="Yamada", age=30)
graph.invoke(initial_state)

以下、出力結果です。

出力:Hello, Yamada. You are an adult.

上記コードでは、UserStateというPydanticモデルを定義し、ユーザーの名前、年齢、成人かどうかの情報を管理しています。
このように、Pydanticモデルをステートとして使用することで、データの構造を明確に定義し、各ノード間でのデータの受け渡しやバリデーションを容易に行うことができます。

環境変数の管理と設定の読み込み

Pydanticは、環境変数の管理や設定の読み込みを簡素化するための機能も提供しています。
pydantic-settingsパッケージを使用することで、環境変数や.envファイルから設定値を読み込み、データのバリデーションや型変換を自動的に行うことができます。

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
    db_user: str
    db_password: str
    db_host: str = "localhost"
    db_port: int = 5432

settings = Settings()
print(settings.db_user)
print(settings.db_password)

.envファイル

db_user=YAMADA_TARO
db_password=password123

以下、出力結果です。

YAMADA_TARO
password123

このコードでは、Settingsクラスが環境変数や.envファイルからデータベースの設定を読み込みます。
model_configで.envファイルのパスとエンコーディングを指定しています。
これにより、環境変数の管理が容易になり、設定値のバリデーションや型変換も自動的に行われます。

まとめ

本記事では、Pydanticの基本的な使い方から高度な機能、そして実践的な応用例までを解説しました。
Pydanticを活用することで、Pythonでのデータバリデーションや型安全性の確保が容易になり、コードの信頼性と可読性が向上します。
様々な場面で応用の利くライブラリと感じますので、今後も使用していきたいと思います。