0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonのType Hint実務で効く書き方と、Pydanticで“実行時に守る”設計へ

Posted at

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 で足す

型ヒントは strint までは表せますが、実務で欲しい制約はこういうもの:

  • 文字数(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)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?