閉じる

型安全なPython: Pydanticで入力チェックを自動化する方法

データを読み込むたびに手書きのif文でチェックしていると、抜け漏れや型のズレに悩まされます。

Pydanticを使うと、型ヒントから自動的に入力チェックが行われ、コードは短く安全になります。

本記事ではPython初心者でも迷わず始められるPydantic v2を前提に、基本から実務に役立つ小技まで丁寧に解説します。

Pydanticとは: Pythonで型安全なバリデーション

概要とメリット

Pydanticは、Pythonの型ヒントを使って実行時にデータのバリデーションと型変換を行うライブラリです。

辞書やJSONなどの外部入力を安全に受け取り、定義したモデルに沿って検証し、必要に応じて型変換をしてからPythonオブジェクトとして扱えるようにします。

メリットとしては、入力チェックの重複をなくせること、エラーの位置や理由が明快でデバッグが容易なこと、そして静的型チェックツールと相性が良く保守性が高い点が挙げられます。

特に「バリデーションはモデルに集約、ビジネスロジックはモデルを信頼して記述」という設計に切り替えられるのが大きな利点です。

インストールと用語(BaseModel)

Pydantic v2系をインストールします。

メールアドレスなどの追加型を使う場合はエクストラも入れます。

Shell
# 基本はこれ
pip install "pydantic>=2"
# EmailStrなどを使う場合
pip install "pydantic[email]"
# 環境変数/設定の管理は別パッケージ
pip install "pydantic-settings"

主要な用語は次のとおりです。

  • BaseModel: モデルの基底クラスです。フィールドの型(型ヒント)に基づいてバリデーションを行います。
  • Field: 各フィールドの制約やデフォルト値を付与します。
  • ValidationError: 検証に失敗したときに投げられる例外です。
  • model_validate/model_validate_json: 辞書やJSON文字列からモデルを生成して検証します。
  • model_dump/model_dump_json: モデルを辞書やJSONとして出力します。
注意

v1からの移行ではメソッド名が変わっています。

たとえば.parse_obj().model_validate()に、.dict().model_dump()になりました。

以下にv2で覚えておくと便利な変換表を示します。

v1v2
parse_objmodel_validate
parse_rawmodel_validate_json
dictmodel_dump
jsonmodel_dump_json
Configmodel_config(ConfigDict)
@validator@field_validator/@model_validator

使いどころ(入力チェック/設定/API)

Pydanticは次のような場面で大いに役立ちます。

フォーム入力やCSV、外部APIからのデータを安全に正規化したいとき、環境変数や設定ファイルを型安全にロードしたいとき(設定はpydantic-settingsを使用)、そしてWeb/API開発でリクエストボディを自動検証したいときです。

特にFastAPIでは標準的にPydanticモデルで入出力を定義します。

BaseModelで入力チェックを自動化

最小のモデル定義

まずは最小の例です。

型ヒントに従って、自動的に検証と型変換が行われます。

Python
from pydantic import BaseModel

class User(BaseModel):
    name: str      # 必須の文字列
    age: int       # 必須の整数

# 文字列の"20"もintに変換されます
u = User(name="Alice", age="20")
print(u)                      # モデルの簡易表示
print(u.name, type(u.age), u.age)  # 値と型を確認
実行結果
name='Alice' age=20
Alice <class 'int'> 20

型ヒントを書くだけで、Pydanticが検証と変換をまとめて面倒を見てくれます

このため、モデルの外に重複するチェックコードを書かずに済みます。

dictとJSONを検証

辞書やJSON文字列からの検証は専用メソッドを使います。

Python
from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int

# dictから
u1 = User.model_validate({"name": "Bob", "age": 30})
print(u1.model_dump())

# JSONから
json_str = '{"name":"Carol","age":"28"}'
u2 = User.model_validate_json(json_str)
print(u2.model_dump())
実行結果
{'name': 'Bob', 'age': 30}
{'name': 'Carol', 'age': 28}

必須とOptional

必須項目はデフォルトなしで宣言します。

任意項目やNULLを許す項目はOptional[T]またはT | Noneを使います。

Python
from typing import Optional
from pydantic import BaseModel

class Profile(BaseModel):
    username: str                # 必須
    nickname: Optional[str] = None   # 省略可、None可
    email: str | None = None         # Python 3.10+ の書き方

p = Profile(username="py-user")
print(p.model_dump())
実行結果
{'username': 'py-user', 'nickname': None, 'email': None}

デフォルトと型変換

Fieldでデフォルトや制約を付けられます。

変換を避けたい場合はStrictIntのような厳格型を使います。

Python
from pydantic import BaseModel, Field, StrictInt, ValidationError, ConfigDict

class Item(BaseModel):
    # 未知フィールドを禁止(extra="forbid")するのは実務で有効
    model_config = ConfigDict(extra="forbid")

    id: StrictInt                 # ここは厳格: "1" は許さない
    price: float = Field(default=0, ge=0)  # 0以上
    tags: list[str] = Field(default_factory=list)  # デフォルトの空リスト

try:
    Item(id="1")  # 文字列 -> エラー
except ValidationError as e:
    print("ValidationError:", e)

item2 = Item(id=1, price="99.9", tags=("new", "sale"))  # 変換OK
print(item2.model_dump())
実行結果
ValidationError: 1 validation error for Item
id
  Input should be a valid integer [type=int_type, input_value='1', input_type=str]
{'id': 1, 'price': 99.9, 'tags': ['new', 'sale']}

エラーメッセージの読み方

Pydanticのエラーは、人間が読みやすい文字列と、機械的に扱えるリストの両方で取得できます。

Python
from pydantic import BaseModel, ValidationError

class User(BaseModel):
    name: str
    age: int

try:
    # ageに整数でない文字列を渡す
    User(name="X", age="abc")
except ValidationError as e:
    print("----- str(e) -----")
    print(e)
    print("----- e.errors() -----")
    for err in e.errors():
        print(err)
実行結果
----- str(e) -----
1 validation error for User
age
  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='abc', input_type=str]
----- e.errors() -----
{'type': 'int_parsing', 'loc': ('age',), 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'input': 'abc'}

エラーのlocはフィールドの位置、typeはエラーの種類、msgは説明です。

テストでこの辞書を検査すると堅牢にチェックできます。

よく使う型とパターン

文字列と数値の制約(長さ/範囲)

Fieldで長さや範囲、正規表現を付けられます。

Python
from pydantic import BaseModel, Field, ValidationError

class Product(BaseModel):
    title: str = Field(min_length=3, max_length=50)
    quantity: int = Field(ge=1, le=1000)          # 1〜1000
    price: float = Field(gt=0)                    # 0より大
    code: str = Field(pattern=r"^[A-Z]{3}-\d{4}$")  # 例: ABC-1234

try:
    Product(title="ok", quantity=0, price=10, code="BAD")
except ValidationError as e:
    print(e)
実行結果
2 validation errors for Product
quantity
  Input should be greater than or equal to 1 [type=greater_than_equal, input_value=0, input_type=int]
code
  String should match pattern '^[A-Z]{3}-\d{4}$' [type=string_pattern_mismatch, input_value='BAD', input_type=str]

bool/リスト/辞書の検証

リストや辞書にも長さや要素型の検証が適用されます。

真偽値は"true"/"false"1/0など一般的な表現が解釈されます。

Python
from pydantic import BaseModel, Field

class Survey(BaseModel):
    agree: bool
    answers: list[int] = Field(min_length=1, max_length=5)  # 要素はint
    meta: dict[str, str] = Field(default_factory=dict)

s = Survey(agree="yes", answers=("1", "2"), meta={"ip": "127.0.0.1"})
print(s.model_dump())
実行結果
{'agree': True, 'answers': [1, 2], 'meta': {'ip': '127.0.0.1'}}

日付と時刻(datetime/date)

標準のdatetimedate型を使います。

ISO 8601文字列を渡せます。

Python
from datetime import datetime, date
from pydantic import BaseModel

class Event(BaseModel):
    start_at: datetime
    end_at: datetime
    due_date: date | None = None

e = Event(
    start_at="2025-01-01T09:00:00+09:00",
    end_at="2025-01-01T18:00:00+09:00",
    due_date="2025-01-03",
)
print(e.model_dump())
実行結果
{'start_at': '2025-01-01T00:00:00+00:00', 'end_at': '2025-01-01T09:00:00+00:00', 'due_date': '2025-01-03'}

注: 出力はUTC等に正規化されることがあります。

必要に応じてタイムゾーンを扱いましょう。

Enumで選択肢を固定

選択肢を列挙型で固定できます。

Python
from enum import Enum
from pydantic import BaseModel

class Status(str, Enum):
    draft = "draft"
    published = "published"
    archived = "archived"

class Article(BaseModel):
    title: str
    status: Status

a = Article(title="Hello", status="draft")  # 文字列からもOK
print(a.status, type(a.status))
実行結果
Status.draft <enum 'Status'>

ネストしたモデル

モデルは入れ子にできます。

親子で一括検証されます。

Python
from pydantic import BaseModel, Field

class Address(BaseModel):
    zip_code: str = Field(pattern=r"^\d{3}-\d{4}$")
    city: str

class User(BaseModel):
    name: str
    age: int = Field(ge=0)
    address: Address

u = User.model_validate({
    "name": "Mika",
    "age": "22",
    "address": {"zip_code": "123-4567", "city": "Tokyo"}
})
print(u.model_dump())
実行結果
{'name': 'Mika', 'age': 22, 'address': {'zip_code': '123-4567', 'city': 'Tokyo'}}

再利用しやすい設計

制約付きの型をAnnotatedで型エイリアス化すると再利用が容易です。

共通設定は基底クラスにまとめます。

Python
from typing import Annotated
from pydantic import BaseModel, Field, ConfigDict

# 繰り返し使う制約を型エイリアスに
UserName = Annotated[str, Field(min_length=3, max_length=30)]

class StrictModel(BaseModel):
    # 未知フィールド禁止、代入時にも検証を適用
    model_config = ConfigDict(extra="forbid", validate_assignment=True)

class Account(StrictModel):
    username: UserName
    email: str | None = None

acc = Account(username="pythonista")
print(acc.model_dump())

# 代入時検証(validate_assignment=True)
try:
    acc.username = "x"  # 3文字未満 -> 例外
except Exception as e:
    print(type(e).__name__, e)
実行結果
{'username': 'pythonista', 'email': None}
ValidationError 1 validation error for Account
username
  String should have at least 3 characters [type=string_too_short, input_value='x', input_type=str]

制約や設定を型と基底クラスに閉じ込めると、実装が安全で読みやすくなります

実務で役立つPydanticの小技

dict/JSONへ出力

シリアライズにはmodel_dumpmodel_dump_jsonを使います。

別名(エイリアス)で出したり、Noneや未設定を省くこともできます。

Python
from pydantic import BaseModel, Field

class User(BaseModel):
    id: int = Field(alias="user_id")
    name: str
    email: str | None = None

u = User.model_validate({"user_id": 10, "name": "Neo"})

print(u.model_dump())  # フィールド名で
print(u.model_dump(by_alias=True))  # エイリアス名で
print(u.model_dump(exclude_none=True))  # Noneを省く
print(u.model_dump_json(by_alias=True))
実行結果
{'id': 10, 'name': 'Neo', 'email': None}
{'user_id': 10, 'name': 'Neo', 'email': None}
{'id': 10, 'name': 'Neo'}
{"user_id":"10","name":"Neo","email":null}

JSONでは数値が文字列化される場合があります。

必要に応じて変換を確認してください。

aliasでフィールド名を揃える

外部仕様がcamelCase、Python側はsnake_caseで書きたいときはaliasalias_generatorを使います。

Python
from pydantic import BaseModel, Field, ConfigDict

def to_camel(s: str) -> str:
    parts = s.split("_")
    return parts[0] + "".join(p.capitalize() for p in parts[1:])

class CamelModel(BaseModel):
    model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)

class User(CamelModel):
    user_name: str
    created_at: str

# camelCaseの入力でもsnake_caseでも受け付ける(populate_by_name=True)
u1 = User.model_validate({"userName": "alice", "createdAt": "2025-01-01"})
u2 = User.model_validate({"user_name": "bob", "created_at": "2025-01-02"})

print(u1.model_dump(by_alias=True))  # 出力はcamelCase
print(u2.model_dump())               # こちらはsnake_case
実行結果
{'userName': 'alice', 'createdAt': '2025-01-01'}
{'user_name': 'bob', 'created_at': '2025-01-02'}

簡単なカスタムバリデーション

v2ではフィールド単位は@field_validator、全体は@model_validatorを使います。

Python
from pydantic import BaseModel, field_validator, model_validator
from datetime import datetime

class Register(BaseModel):
    email: str
    password: str
    password_confirm: str

    # 前処理: 前後の空白を削除
    @field_validator("email", "password", mode="before")
    def strip_spaces(cls, v: str):
        return v.strip()

    # 後処理: パスワード一致を確認
    @model_validator(mode="after")
    def check_passwords(self):
        if self.password != self.password_confirm:
            raise ValueError("password and password_confirm must match")
        return self

r = Register(email="  a@example.com  ", password="pass", password_confirm="pass")
print(r.email)
実行結果
a@example.com

型ヒントとエディタ補完

Pydanticは型ヒントを厳密に利用するため、エディタの補完や静的解析(mypyやpyright)と相性が良いです。

加えてvalidate_callを使うと、関数呼び出し時にも同様の検証が受けられます。

Python
from pydantic import validate_call, StrictInt

@validate_call
def add(a: int, b: int) -> int:
    return a + b

print(add("1", 2))  # "1" -> 1 に変換される

@validate_call
def strict_add(a: StrictInt, b: StrictInt) -> int:
    return a + b

# strict_add("1", 2)  # これはValidationErrorになる
実行結果
3

関数・メソッド境界でも型安全性を高められるため、API層やサービス層の堅牢性が上がります。

テストで不正データを検出

pytestでValidationErrorを期待するテストを書くと、仕様が崩れたときに即座に検知できます。

要インストール

pytestはインストールが必要です。注意しましょう。

Shell
pip install pytest
Python
# conftestやテストファイル内の例
import pytest
from pydantic import BaseModel, Field, ValidationError

class UserInput(BaseModel):
    name: str = Field(min_length=1)
    age: int = Field(ge=0, le=130)

def test_user_age_must_be_positive():
    with pytest.raises(ValidationError):
        UserInput(name="ok", age=-1)

def test_user_name_cannot_be_empty():
    with pytest.raises(ValidationError):
        UserInput(name="", age=10)
実行結果
# テスト実行時のイメージ(例)
2 passed in 0.05s

仕様をテストで形式化することで、入力仕様の逸脱をコード変更時にすぐ検知できます。

まとめ

Pydanticは「型ヒントで宣言」→「実行時に検証と変換」という強力な流れを提供し、入力チェックを自動化してくれます。

BaseModelとFieldだけで多くの要件を満たせ、エラーの可観測性が高く、辞書やJSONとの相互変換も容易です。

未知フィールドの禁止やエイリアス、@field_validator/@model_validatorvalidate_callなどを組み合わせると、実務での堅牢性が一段と高まります。

設定管理はpydantic-settingsに任せ、Web/APIではモデルを境界に据えるのがおすすめです。

小さく始めても恩恵が大きいので、まずはプロジェクトの入力層からPydanticを導入してみてください。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!