Pythonの型ヒント(type hint)は、**コードを読みやすくし、IDE補完や静的解析(mypy / pyright)を強化するための“設計情報”**です。一方で Pydantic は、その型ヒントを読み取って「実行時に」入力の検証・変換・エラーメッセージ生成まで行うライブラリです。
この記事では、type hintの基本から実務で使う型表現、そしてPydanticとの関係までを、例を多めにまとめます。
1. Type Hintとは?(誤解ポイント)
-
型ヒントは 基本的に実行時の型強制ではない
-
Python本体が型を守ってくれるわけではなく、主に以下に効く
- IDEの補完・リファクタ
- 静的解析(mypy/pyright)
- チーム開発での意図共有
def add(x: int) -> int:
return x + 1
add("1") # 型ヒントだけでは事前に止められない(静的解析なら検出できる)
2. まずは基本:変数 / 関数 / 戻り値
2.1 変数
name: str = "Taro"
age: int = 30
pi: float = 3.14159
is_active: bool = True
2.2 関数
def add(a: int, b: int) -> int:
return a + b
def greet(name: str) -> None:
print(f"Hello {name}")
3. コレクション:list / dict / set / tuple
Python 3.9+ では list[int] のように組み込み型へ直接書けます。
nums: list[int] = [1, 2, 3]
user_by_id: dict[int, str] = {1: "alice", 2: "bob"}
tags: set[str] = {"python", "typing"}
point: tuple[int, int] = (10, 20)
rgb: tuple[int, int, int] = (255, 128, 0) # 固定長tuple
4. Optional(Noneを取りうる)— 実務で最重要
None を返す/受け取る可能性があるなら、T | None(= Optional)を明示します。
def find_user(user_id: int) -> str | None:
if user_id == 1:
return "alice"
return None
呼び出し側はNoneチェックで安全に:
u = find_user(2)
if u is None:
print("not found")
else:
print(u.upper())
5. Union(複数型)とnarrowing(絞り込み)
def stringify(x: int | float | str) -> str:
if isinstance(x, (int, float)):
return f"{x:.2f}"
return x
6. Literal(値を型にする)— 設定/モードのバグ防止
from typing import Literal
Mode = Literal["dev", "prod"]
def load_config(mode: Mode) -> dict[str, str]:
return {"mode": mode}
7. 型エイリアス:意味を名前にして読みやすく
UserId = int
Email = str
def send_welcome(user_id: UserId, email: Email) -> None:
...
8. TypedDict:dictの“形”を固定(JSON境界で強い)
from typing import TypedDict, NotRequired
class User(TypedDict):
id: int
name: str
email: str
class User2(TypedDict):
id: int
name: str
nickname: NotRequired[str]
9. dataclass:モデル表現の定番(ドメイン層におすすめ)
from dataclasses import dataclass
@dataclass(frozen=True)
class Product:
id: int
name: str
price: int
def total(p: Product, qty: int) -> int:
return p.price * qty
10. Protocol:Duck Typingを型安全に
「このメソッドがあればOK」という設計ができます。
from typing import Protocol
class SupportsClose(Protocol):
def close(self) -> None: ...
def cleanup(x: SupportsClose) -> None:
x.close()
11. Generics:ユーティリティ関数で必須
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
辞書のキー/値を扱う例:
K = TypeVar("K")
V = TypeVar("V")
def invert(d: dict[K, V]) -> dict[V, K]:
return {v: k for k, v in d.items()}
12. Callable:関数を引数に取る
from collections.abc import Callable
def apply(xs: list[int], f: Callable[[int], int]) -> list[int]:
return [f(x) for x in xs]
apply([1, 2, 3], lambda x: x * 2)
13. Iterable / Sequence / Mapping:受け口を柔らかく
「何を要求するか」で型を選ぶと、再利用性が上がります。
from collections.abc import Iterable, Sequence, Mapping
def sum_all(xs: Iterable[int]) -> int:
return sum(xs)
def head(xs: Sequence[int]) -> int:
return xs[0]
def keys(m: Mapping[str, int]) -> list[str]:
return list(m.keys())
14. Generator(yield)に型を付ける
from collections.abc import Generator
def countdown(n: int) -> Generator[int, None, None]:
while n > 0:
yield n
n -= 1
15. overload:引数で戻り値が変わる時
from typing import overload, Literal
@overload
def parse(v: str, *, as_int: Literal[True]) -> int: ...
@overload
def parse(v: str, *, as_int: Literal[False]) -> str: ...
def parse(v: str, *, as_int: bool) -> int | str:
return int(v) if as_int else v
16. NewType:同じintでも“意味”を分ける
from typing import NewType
UserId = NewType("UserId", int)
OrderId = NewType("OrderId", int)
def fetch_user(uid: UserId) -> None:
...
fetch_user(UserId(1))
17. Annotated:メタ情報(制約や説明)を載せる
from typing import Annotated
PositiveInt = Annotated[int, "must be positive"]
def set_limit(limit: PositiveInt) -> None:
...
(FastAPIなどで特に活躍)
18. TypeGuard:自作の型判定で絞り込み
from typing import TypeGuard
def is_str_list(x: object) -> TypeGuard[list[str]]:
return isinstance(x, list) and all(isinstance(i, str) for i in x)
val: object = ["a", "b"]
if is_str_list(val):
print(val[0].upper())
19. クラス/メソッドの型(実務の基本)
class UserRepo:
def __init__(self, base_url: str) -> None:
self.base_url = base_url
def get(self, user_id: int) -> dict[str, str]:
return {"id": str(user_id)}
@staticmethod
def normalize_email(email: str) -> str:
return email.strip().lower()
20. ここから本題:Type Hint と Pydantic の関係
20.1 役割の違い(静的 vs 実行時)
- Type hint:主に静的(IDE/型チェッカー/可読性)
- Pydantic:型ヒントを“仕様”として読み、実行時に検証・変換する
つまり、型ヒントを「約束事」に、Pydanticを「門番」にできます。
20.2 Pydanticは型ヒントを使って入力を整形する
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
is_admin: bool = False
u = User.model_validate({"id": "123", "name": "Alice"})
print(u.id) # 123(intに変換)
print(u.is_admin) # False(デフォルト)
変換できない・欠けている・不正な形式なら ValidationError で止まります。
→ **外部入力(JSON/環境変数/DB/Queue)**の“境界”で特に強い。
20.3 Optional / Union / nested をそのまま理解してくれる
from pydantic import BaseModel
class Address(BaseModel):
city: str
zip: str
class User(BaseModel):
name: str
tags: list[str]
address: Address
note: str | None = None
price: int | float
20.4 型ヒントだけでは言いにくい「制約」を Field で足す
型ヒントは str や int までは表せますが、実務で欲しい制約はこういうもの:
- 文字数(min/max)
- 数値の範囲(0以上など)
- 正規表現
- デフォルト値
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(min_length=1, max_length=50)
price: int = Field(ge=0)
sku: str = Field(pattern=r"^[A-Z0-9\-]+$")
20.5 FastAPIは「型ヒント × Pydantic」で自動検証&スキーマ生成
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class CreateUser(BaseModel):
name: str
age: int
@app.post("/users")
def create_user(body: CreateUser) -> dict[str, str]:
return {"name": body.name}
- リクエストbodyを自動で検証
- 不正なら 422 などで説明つきエラー
- OpenAPI(Swagger)も自動生成
21. 実務で破綻しないための“使い分け”指針
21.1 基本方針
- アプリ内部(純粋ロジック):type hint中心(dataclass / Protocol / Generics 等)
- 境界層(外部入力):Pydanticで「検証・変換」してから内部へ渡す
この分離をすると、内部ロジックが「常に正しい形のデータ」を前提にできて超楽になります。
21.2 ありがちな構成
- Controller/Handler:Pydanticで入力をvalidate
- Service:type hintで契約を明示(内部は“正しい型”前提)
- Domain:dataclass / Protocol 等で表現
- DTO:TypedDictやPydantic Modelで境界を固める
22. まとめ
- Type Hintは“設計情報”。静的解析と可読性・保守性を上げる
- Pydanticは型ヒントを“仕様”として読み、実行時の検証・変換を担う
- 実務の肝は「境界でPydantic、内部は型ヒントで設計」を徹底すること
付録:最小チートシート
-
x: T | None… None許容 -
list[T],dict[K, V]… コレクション -
Literal["a","b"]… 値を固定 -
TypedDict… dictの形 -
@dataclass… ドメインモデル -
Protocol… “このメソッドがあればOK” -
TypeVar… ジェネリクス - Pydantic
BaseModel… validate/parseの門番 -
Field(...)… 制約(範囲/長さ/regex)