Pydantic入門 – Pythonでシンプルかつ強力なバリデーションを始めよう


## はじめに
この記事では、Pythonのデータバリデーションライブラリである**Pydantic**を使って、簡単にかつ強力にデータのバリデーションを行う方法を解説します。  
今回は**Google Colab**上でハンズオン形式で進めていきますので、ブラウザさえあれば実行環境を整えるのも簡単です。

---

## 1. Pydanticとは？
- **データのバリデーション**や型の宣言を簡単に行えるPythonライブラリ
- **Pythonの型ヒント**(type hints)を活用して、データ構造の定義と検証を同時に実現
- **FastAPI**などの人気フレームワークでも広く採用されており、API開発・プロジェクト構成などで非常に便利

Pydanticを使うと、辞書やJSONで受け取ったデータが正しい形式になっているかどうかを**Pythonic**に検証できるようになります。データの整合性を保つために煩雑なチェックを書かなくてよくなるので、とてもおすすめです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/7a274047-7773-1520-d4de-e2fc6bc3798c.png)

---

## 2. Google Colabでの開発準備
まずはGoogle Colabにアクセスし、新しいノートブックを作成します。無料のGoogleアカウントがあれば簡単に利用できます。

### 2-1. ライブラリのインストール
Google Colabのセルで次のようにして、Pydanticをインストールしましょう。Colab環境は再起動するとインストール済みのライブラリが消えてしまうため、ノートブックを開くたびに必要に応じてインストールします。

```python
!pip install pydantic
```

---
## 3. はじめてのPydanticモデル作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/37a0c7bb-63ec-2c98-9677-cac7dc2b1ae5.png)

Pydanticの基本は**BaseModel**クラスを継承してモデルクラスを作ることです。以下の例では、ユーザープロファイルを表すモデルを定義します。

```python
from pydantic import BaseModel

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

- **BaseModel**を継承したクラスに型ヒント付きで属性を定義する
- インスタンス化するときに、型や必須項目のチェックが自動で行われる

### 3-1. 簡単な動作確認
実際にインスタンスを作成して動作を確認してみましょう。

```python
# 正しいデータの場合
valid_data = {
    "name": "Alice",
    "age": 25,
    "email": "alice@example.com"
}
user = User(**valid_data)
print(user)
```

- `User(**valid_data)`で辞書を展開して引数を渡します。
- `print(user)`すると、`name='Alice' age=25 email='alice@example.com'`のようにバリデーションを通ったデータが表示されます。

```python
# 間違ったデータの場合
invalid_data = {
    "name": "Bob",
    "age": "not a number",
    "email": "bob@example.com"
}
user = User(**invalid_data)
```

- `age`が文字列になっているためバリデーションエラー (`ValidationError`) が発生します。
- エラーにはどのフィールドがどのように不正なのかが表示されるので、デバッグにも便利です。

---

## 4. バリデーションの仕組みと例外処理
Pydanticはインスタンス生成時にバリデーションを行い、不正なデータがあれば例外を投げます。エラーハンドリングしたい場合は、`try-except`で囲んで処理しましょう。

```python
from pydantic import ValidationError

try:
    user = User(**invalid_data)
except ValidationError as e:
    print("Validation Error:")
    print(e)
```

- **ValidationError**オブジェクトには、エラー詳細が格納されている
- どのフィールドでバリデーションに失敗したかがわかりやすい

---

## 5. 追加バリデーション – カスタムバリデータ
Pydanticには、**カスタムバリデータ**を定義するための便利な仕組みが用意されています。  
たとえば、`@validator`デコレータを使って、`email`フィールドのドメインを限定するといったことが可能です。

```python
from pydantic import BaseModel, validator

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

    @validator('email')
    def email_must_be_certain_domain(cls, v):
        if not v.endswith("@example.com"):
            raise ValueError("Email must be on the 'example.com' domain.")
        return v
```

- `@validator('email')`によって、`email`フィールドに対するバリデーションを定義
- 条件を満たさない場合は**ValueError**を発生させる

試しに次のコードを実行すると、正しいアドレスと誤ったアドレスで結果が変わることを確認できます。

```python
try:
    user_ok = User(name="Carol", age=30, email="carol@example.com")
    print("バリデーション成功:", user_ok)
    
    user_ng = User(name="Dave", age=40, email="dave@gmail.com")
except ValidationError as e:
    print("バリデーション失敗:", e)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/de56c991-6338-3b4e-ea26-86ec1f2b1773.png)

---

## 6. モデル同士の入れ子（ネスト）を試してみる
複雑なデータ構造を扱いたい場合は、モデルを入れ子にすることができます。  
たとえば、`Address`モデルと`User`モデルを別々に定義して、`User`モデルの中に`Address`モデルを持たせるイメージです。

```python
from pydantic import BaseModel
from typing import Optional

class Address(BaseModel):
    city: str
    zip_code: str

class UserWithAddress(BaseModel):
    name: str
    age: int
    address: Optional[Address] = None
```

- `address`フィールドには`Address`を格納
- `Optional[Address]`にすることで、住所情報が無い場合（`None`）でもOKに

```python
data_with_address = {
    "name": "Emily",
    "age": 28,
    "address": {
        "city": "Tokyo",
        "zip_code": "100-0001"
    }
}

user_with_address = UserWithAddress(**data_with_address)
print(user_with_address)
```

---

## 7. まとめ
- **Pydantic**を使うと、型ヒントを活用して簡潔にデータバリデーションが可能
- **ValidationError**でエラー内容が明確になるため、デバッグやエラー処理もしやすい
- **Google Colab**ならすぐに環境を整えてハンズオンができる
- さらに複雑なモデルやカスタムバリデーションを組み合わせることで、堅牢なデータ構造を実現できる

今回はPydanticの超入門編として、基本的な使い方を紹介しました。次回以降では、**ユニオン型**を使った柔軟なバリデーションや、**設定クラス**を使った細かい挙動制御、**FastAPI**などでの活用方法などを取り上げていきたいと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/9e28a2d8-ec57-698e-614f-864fa92ff1bf.png)

---

## 参考リンク
- [Pydantic公式ドキュメント](https://docs.pydantic.dev/)
- [FastAPI公式ドキュメント](https://fastapi.tiangolo.com/)


## 動作確認用ノートブック

```json
{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "41150395",
   "metadata": {},
   "source": [
    "# Pydantic 動作確認ノートブック\n",
    "\n",
    "このノートブックでは、Pydanticの基本的な機能を動作確認するためのコードを実行します。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "34c16a2f",
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install pydantic"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8de996d2",
   "metadata": {},
   "source": [
    "## Pydanticモデルの作成\n",
    "\n",
    "まず、基本的なPydanticモデルを作成して動作を確認します。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cab5fadb",
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "from pydantic import BaseModel\n",
    "\n",
    "class User(BaseModel):\n",
    "    name: str\n",
    "    age: int\n",
    "    email: str\n",
    "\n",
    "# 正しいデータ\n",
    "valid_data = {\n",
    "    \"name\": \"Alice\",\n",
    "    \"age\": 25,\n",
    "    \"email\": \"alice@example.com\"\n",
    "}\n",
    "\n",
    "user = User(**valid_data)\n",
    "print(user)\n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c56b40bc",
   "metadata": {},
   "source": [
    "## バリデーションエラーの確認\n",
    "\n",
    "不正なデータを与えるとバリデーションエラーが発生します。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "80789ccf",
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "from pydantic import ValidationError\n",
    "\n",
    "# 間違ったデータ\n",
    "invalid_data = {\n",
    "    \"name\": \"Bob\",\n",
    "    \"age\": \"not a number\",\n",
    "    \"email\": \"bob@example.com\"\n",
    "}\n",
    "\n",
    "try:\n",
    "    user = User(**invalid_data)\n",
    "except ValidationError as e:\n",
    "    print(\"Validation Error:\")\n",
    "    print(e)\n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13daf9b9",
   "metadata": {},
   "source": [
    "## カスタムバリデータの例\n",
    "\n",
    "次に、`@validator`デコレータを使ったカスタムバリデータの例を試してみます。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dde47d60",
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "from pydantic import validator\n",
    "\n",
    "class UserWithCustomValidation(BaseModel):\n",
    "    name: str\n",
    "    age: int\n",
    "    email: str\n",
    "\n",
    "    @validator('email')\n",
    "    def validate_email(cls, value):\n",
    "        if not value.endswith(\"@example.com\"):\n",
    "            raise ValueError(\"Email must be on the 'example.com' domain.\")\n",
    "        return value\n",
    "\n",
    "# 正しいデータ\n",
    "user_valid = UserWithCustomValidation(name=\"Alice\", age=25, email=\"alice@example.com\")\n",
    "print(user_valid)\n",
    "\n",
    "# 間違ったデータ\n",
    "try:\n",
    "    user_invalid = UserWithCustomValidation(name=\"Bob\", age=30, email=\"bob@gmail.com\")\n",
    "except ValidationError as e:\n",
    "    print(\"Validation Error:\")\n",
    "    print(e)\n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "885c372e",
   "metadata": {},
   "source": [
    "## 入れ子モデルの例\n",
    "\n",
    "Pydanticでは入れ子モデルも簡単に扱えます。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "35275b4b",
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "from typing import Optional\n",
    "\n",
    "class Address(BaseModel):\n",
    "    city: str\n",
    "    zip_code: str\n",
    "\n",
    "class UserWithAddress(BaseModel):\n",
    "    name: str\n",
    "    age: int\n",
    "    address: Optional[Address] = None\n",
    "\n",
    "data_with_address = {\n",
    "    \"name\": \"Emily\",\n",
    "    \"age\": 28,\n",
    "    \"address\": {\n",
    "        \"city\": \"Tokyo\",\n",
    "        \"zip_code\": \"100-0001\"\n",
    "    }\n",
    "}\n",
    "\n",
    "user_with_address = UserWithAddress(**data_with_address)\n",
    "print(user_with_address)\n",
    "    "
   ]
  }
 ],
 "metadata": {},
 "nbformat": 4,
 "nbformat_minor": 5
}
```