dataclass vs Pydantic — どちらを選ぶべきか
概要
Python でデータを「型つきの構造体」として扱いたいとき、標準ライブラリの dataclasses とサードパーティの Pydantic のどちらを使うかは、よくある判断ポイントです。見た目が似ているため混同されがちですが、設計思想はまったく異なります。本記事では、コード例を交えて両者の違いを整理し、状況別の選び方を示します。
結論を先に書きます。
- 内部データの入れ物なら
dataclass(軽量・標準・ゼロ依存) - 外部からの入力を検証・変換する境界なら
Pydantic(バリデーション・型強制・シリアライズ)
本記事のコードは Python 3.11 / Pydantic 2.x を前提にしています。Pydantic は v1 と v2 で API が大きく異なるため、v2 を前提にします。
根本的な違い:実行時に検証するか
一番大きな違いは 「実行時に検証するかどうか」 です。
dataclass は __init__ や __repr__ などのボイラープレートを自動生成するコードジェネレータにすぎません。型アノテーションは静的解析のためのヒントであって、実行時には一切チェックされません。
from dataclasses import dataclass
@dataclass
class User:
id: int
name: str
# int のはずの id に文字列を渡しても、dataclass は何も言わない
u = User(id="not-an-int", name=123)
print(u) # User(id='not-an-int', name=123)
一方 Pydantic は、インスタンス生成時に型を検証し、必要なら変換(型強制)します。
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
print(User(id="10", name="Alice")) # id=10 name='Alice'("10"→10)
User(id="abc", name="Alice") # ValidationError
この一点だけでも、使いどころが分かれます。
比較表
| 観点 | dataclass | Pydantic (v2) |
|---|---|---|
| 標準ライブラリ | ○(dataclasses) |
×(外部依存) |
| 実行時バリデーション | なし | あり |
| 型強制(coercion) | なし | あり(strict で抑制可) |
| JSON シリアライズ | 手動(asdict 等) |
model_dump() / model_dump_json() |
| JSON デシリアライズ | 手動 | model_validate() |
| ネスト構造の検証 | 自前実装 | 自動で再帰検証 |
| JSON Schema 出力 | なし | model_json_schema() |
| 生成速度 | 速い | 検証コストが乗る |
| エコシステム | 標準 | FastAPI 等と密結合 |
dataclass が向くケース
1. 完全に内部で閉じたデータの入れ物
関数の戻り値をまとめたい、設定値をグルーピングしたいといった「自分のコードの中でしか触らないデータ」には dataclass が最適です。外部入力ではないので検証は不要で、依存も増えません。
from dataclasses import dataclass, field
@dataclass(frozen=True, slots=True)
class Point:
x: float
y: float
@dataclass
class Order:
items: list[str] = field(default_factory=list)
total: float = 0.0
slots=True(3.10+)を付けるとメモリ効率と属性アクセスが向上します。大量生成する値オブジェクトに有効です。
2. 依存を増やしたくないライブラリ・CLI
配布するライブラリや小さな CLI ツールで「Pydantic を追加するほどではない」場合、標準の dataclass なら依存ゼロで済みます。
3. 検証は型チェッカで十分な場面
入力元が自分のコードに限られ、mypy / pyright で型が担保できるなら、実行時検証のコストを払う理由はありません。
Pydantic が向くケース
1. 外部入力の境界
HTTP リクエストボディ、環境変数、設定ファイル、外部 API のレスポンスなど、形が崩れうる入力は、境界で一度 Pydantic に通せば以降は型が保証されたオブジェクトとして安心して扱えます。
from pydantic import BaseModel, Field, field_validator
class SignupRequest(BaseModel):
email: str
age: int = Field(ge=0, le=150)
tags: list[str] = []
@field_validator("email")
@classmethod
def must_have_at(cls, v: str) -> str:
if "@" not in v:
raise ValueError("invalid email")
return v
req = SignupRequest.model_validate(
{"email": "a@example.com", "age": "30", "tags": ["x"]}
)
2. ネストした構造をまとめて検証したい
class Address(BaseModel):
city: str
zipcode: str
class Company(BaseModel):
name: str
address: Address # ネストも自動検証
members: list[SignupRequest]
Pydantic はネストしたモデルを再帰的に検証します。dataclass で同じことをやると各階層に手で検証を書くことになります。
3. シリアライズ / JSON Schema が欲しい
model_dump() / model_dump_json() でのシリアライズ、model_json_schema() での JSON Schema 自動生成は、API ドキュメント生成やフロントとの契約定義で武器になります。FastAPI が Pydantic を採用しているのはこの流れです。
中間の選択肢:Pydantic の dataclass
Pydantic には「dataclass の見た目のまま検証だけ足す」オプションがあります。既存コードから段階的に移行したいときに便利です。
from pydantic.dataclasses import dataclass
@dataclass
class User:
id: int
name: str
User(id="10", name="Alice") # 検証・変換が効く
全面的に BaseModel へ移すのをためらうケースの折衷案になります。
パフォーマンスの注意点
Pydantic v2 はコア部分が Rust 製(pydantic-core)になり、v1 から大幅に高速化しました。とはいえ「検証する」以上、ただ代入するだけの dataclass よりは必ずコストが乗ります。
- ホットパスで大量にオブジェクトを作るなら dataclass(+
slots) - 境界で 1 回検証して以降は内部で使い回すなら Pydantic でも体感差はほぼない
「Pydantic は遅いから避ける」ではなく、「検証が要らない場所で検証を払わない」という発想が正しいです。
確認方法
from dataclasses import dataclass
@dataclass
class D:
n: int
print(D(n="x")) # エラーにならない → D(n='x')
from pydantic import BaseModel
class P(BaseModel):
n: int
print(P(n="5")) # n=5(変換される)
print(P(n="x")) # ValidationError
型強制を厳しくしたい場合は model_config = ConfigDict(strict=True) を設定すると、"5" → 5 のような暗黙変換を禁止できます。
注意点
- Pydantic v1 と v2 で API が別物です(
parse_obj→model_validate、.dict()→model_dump()、@validator→@field_validator)。古い記事のコピペは動きません。 - 可変デフォルト(list / dict)は dataclass では
default_factory必須です。 - 「とりあえず全部 Pydantic」も「頑なに dataclass だけ」も極端です。レイヤごとに使い分けるのが本筋です。
まとめ
- 判断軸は一つ、「実行時に検証が要るか」。
- 内部データ・値オブジェクト・依存を増やしたくない場所 → dataclass(
slots=Trueも検討) - 外部入力の境界・シリアライズ・スキーマ生成 → Pydantic v2
- 迷ったら「アプリの外周は Pydantic、内側は dataclass」という二層構成が無難
- 段階移行には
pydantic.dataclasses.dataclassという折衷案もある
道具としての優劣ではなく、「そのデータが信用できる入力か」で選ぶと迷いません。
