目次
9 章

typing 本格 — Generic、Protocol、TypedDict、Literal

基礎の型ヒントの次の段階 — 型をパラメータ化する Generic、ダックタイピングを正確に書く Protocol、dict の形を明示する TypedDict、狭い union の Literal までを整理します。

第8章 dataclass と __slots__ でデータの形を短く書く道具を見たなら、本章は その形をより正確に、表現力豊かに書く 道具たちです。typing モジュールの本格的な武器4つ — Generic、Protocol、TypedDict、Literal を扱います。

本章の4つの道具は、本書の後半のほとんどに登場します。Protocol は第15章 マジックメソッド深掘りとプロトコル の表面に乗る抽象であり、Generic は第20章 typing 上級 — Variance、ParamSpec、Self、overload の出発点です。TypedDict と Literal の組み合わせは、4部 FastAPI で第24章 Pydantic v2 深掘り の discriminated union として再登場します。

出発点 — どこまでが基礎だったか #

第2章 変数、基本型と型ヒント で次まで押さえました。

  • intstrboolNone
  • list[int]dict[str, int]
  • int | None(union 短縮)
  • type Alias = int(型エイリアス)
  • Callable[[int, int], int]

次の段階 — ユーザーがパラメータを埋められる型 から始めます。

Generic — 型パラメータ #

同じ形なのに中に入る型だけ違う関数 / クラスを書きたいとき。

関数 — 入力の型をそのまま返す #

🚫 型が広すぎる
def first(items: list) -> object:
    return items[0]

x = first([1, 2, 3])    # x: object  ← int という情報を失う

first([1,2,3]) は明らかに int を返しますが、シグネチャが object なのでエディタがその情報を失います。パラメータ化が必要です。

✅ 型パラメータ (3.12+)
def first[T](items: list[T]) -> T:
    return items[0]

x = first([1, 2, 3])      # x: int
y = first(["a", "b"])     # y: str

def first[T] の形が Python 3.12 の新文法です。PEP 695。以前は次のように書きました。

旧方式 — 新コードでは使わない
from typing import TypeVar

T = TypeVar("T")

def first(items: list[T]) -> T:
    return items[0]

新コードは メソッド / 関数の横の [T] の位置に直接書く方式 で統一すればよいです。

複数の型パラメータ #

2 つ以上
def pair[K, V](key: K, value: V) -> tuple[K, V]:
    return (key, value)

p = pair("name", 42)    # tuple[str, int]

型制約 — bound / 明示的な制限 #

bound — 上限の型
class HasLength:
    def __len__(self) -> int: ...

def shortest[T: HasLength](items: list[T]) -> T:
    return min(items, key=len)

[T: HasLength] は「T は HasLength のサブタイプでなければならない」という制約です。実は上の場所には Protocol の方が合います。(下で扱う)

明示的な union 制約
def add[T: (int, float)](a: T, b: T) -> T:
    return a + b

add(1, 2)      # int
add(1.0, 2.0)  # float
add(1, 2.0)    # ✗ T が一つの型に決まらない

[T: (int, float)] の形は「T は int または float」を意味します。ジェネリクスの制約 (constraint) と呼びます。

クラス — Generic class #

ジェネリッククラス (3.12+)
class Stack[T]:
    def __init__(self):
        self._items: list[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T | None:
        if not self._items:
            return None
        return self._items.pop()

s: Stack[int] = Stack()
s.push(1)
s.push(2)
x = s.pop()    # x: int | None

Stack[int] のように 使用時に T を埋めて 使うのがジェネリッククラスの使い方です。Variance(共変・反変・不変)のような話題は第20章 typing 上級 で扱います。

Protocol — ダックタイピングを型に #

Python の慣用句は 「どのクラスのインスタンスか」ではなく「どのメソッド / 属性を持つか」 です。これを静的型で表現するのが Protocol です。

Java / C# のインターフェースではない #

Java のインターフェースは 明示的に implements を書かないと そのインターフェースになりません。Python の Protocol はそれが必要ありません。形さえ合えば その Protocol を満たします (structural typing)。

Protocol の定義
from typing import Protocol

class Closable(Protocol):
    def close(self) -> None: ...

# 何が Closable か?
def safe_close(resource: Closable) -> None:
    resource.close()

# 上の関数は .close() を持つすべてのオブジェクトを受け取る。
# 明示的な継承関係は不要。
class File:
    def close(self) -> None:
        print("file closed")

class Connection:
    def close(self) -> None:
        print("connection closed")

safe_close(File())         # OK — File は Closable を満たす
safe_close(Connection())   # OK — Connection も満たす

runtime_checkable — ランタイムで isinstance 可能 #

デフォルトの Protocol は静的検査用です。isinstance で検査するにはデコレータを付けます。

ランタイムチェック
from typing import Protocol, runtime_checkable

@runtime_checkable
class Closable(Protocol):
    def close(self) -> None: ...

print(isinstance(File(), Closable))   # True

ただし isinstance はメソッド名のみ検査 し、シグネチャは見ません。正確な検査は静的型チェッカーの方が得意です。

Protocol が役立つ場面 — 「必要な分だけ約束」 #

実用例 — sized と iterable
from typing import Protocol

class Sized(Protocol):
    def __len__(self) -> int: ...

def first_n[T](items: Sized, n: int) -> ...:
    if len(items) < n:
        raise ValueError("足りない")
    ...

関数が必要なのは __len__ メソッドのみ。だから引数の型を「リスト」ではなく「len 可能な何か」で表現する方が正確で柔軟です。呼び出し側が list / tuple / dict / ユーザー定義クラスのどれを渡しても通ります。

collections.abc がすでに持つ Protocol #

標準ライブラリにはよく使う Protocol がすでにあります。

よく使う ABC たち
from collections.abc import (
    Iterable, Iterator, Sized, Container,
    Mapping, Sequence, Callable, Hashable
)

def process(items: Iterable[int]) -> None:
    for x in items:
        ...

def lookup(m: Mapping[str, int], key: str) -> int | None:
    return m.get(key)

collections.abc は abstract base class ですが Protocol のように動作することもあり (@runtime_checkable 可能)、標準にあるものはここから持ってきて使えばよいです。 自分で Protocol を定義するのは標準にない形のときです。

TypedDict — dict の形を型に #

JSON のようなデータを dict で扱うとき、どのキーとどの値の型 が入るかを書きたいとき。

TypedDict の基本
from typing import TypedDict

class User(TypedDict):
    id: int
    name: str
    age: int

u: User = {"id": 1, "name": "カーティス", "age": 30}
print(u["name"])   # name: str — 型が絞り込まれる

TypedDictランタイムでは普通の dict です。インスタンス検査をしても dict として見えます。型チェッカーだけが認識します。

dataclass との違い #

dataclassTypedDict
ランタイムの形ユーザー定義クラスのインスタンス通常の dict
メソッド__init____repr____eq__ 自動なし (dict のメソッドのみ)
アクセスu.nameu["name"]
合う場所内部のドメインモデルJSON、外部 API のレスポンス

API のレスポンスを受け取って dict で扱う場所に TypedDict が合います。変換コストがありません — 入ってきた dict をその形だと宣言するだけです。

オプションのキー — total=False / NotRequired #

オプションのキー
from typing import TypedDict, NotRequired

class User(TypedDict):
    id: int                       # 必須
    name: str                      # 必須
    nickname: NotRequired[str]     # 任意

u1: User = {"id": 1, "name": "カーティス"}                       # OK
u2: User = {"id": 1, "name": "カーティス", "nickname": "C"}     # OK

旧方式はクラス単位の total=False ですが、全キーをオプションにしてしまうので ほとんど使いません。新コードは NotRequired が標準です。

逆に、すべてオプションの dict で一部だけ必須にしたいときは Required:

Required の使用
from typing import TypedDict, Required

class Config(TypedDict, total=False):
    timeout: int
    retries: int
    base_url: Required[str]    # これだけ必須

継承もできる #

継承
class BaseUser(TypedDict):
    id: int
    name: str

class AdminUser(BaseUser):
    permissions: list[str]

AdminUseridnamepermissions の3キーを持ちます。

Literal — 狭い union #

値そのものを型に。特定の文字列 / 数字だけ許可したいとき。

Literal の基本
from typing import Literal

def set_log_level(level: Literal["debug", "info", "warning", "error"]) -> None:
    ...

set_log_level("info")     # OK
set_log_level("trace")    # ✗ 許可された 4 つにない

呼び出し側の自動補完が正確に出て、タイポがコンパイル時に捕まります。

Literal と union #

複数の Literal
type Color = Literal["red", "green", "blue"]
type Mode = Literal["light", "dark", "auto"]

def render(color: Color, mode: Mode) -> str:
    ...

定数と組み合わせ — Final #

Final
from typing import Final, Literal

DEFAULT_LEVEL: Final = "info"
# DEFAULT_LEVEL の型が Literal["info"] に絞られる

DEFAULT_LEVEL = "warn"   # ✗ Final — 再代入不可

Final は「この変数は一度だけ代入」を表します。モジュール定数によく使います。

Discriminated union — 決定的な分岐 #

異なる形の dict が一つの場所に入ってくるとき、「どの形か」を一つのキーで識別するパターン。

discriminated union
from typing import Literal, TypedDict

class ClickEvent(TypedDict):
    type: Literal["click"]
    x: int
    y: int

class KeyEvent(TypedDict):
    type: Literal["key"]
    code: str

type Event = ClickEvent | KeyEvent

def handle(event: Event) -> None:
    if event["type"] == "click":
        # ここで event は ClickEvent に絞られる
        print(event["x"], event["y"])
    else:
        # こちらは KeyEvent
        print(event["code"])

type キーの Literal 値を見て型チェッカーが 自動で絞り込みます。 第3章 制御フローmatch-case とよく合うパターンで、第13章 パターンマッチング深掘り で改めて詳しく扱います。

他によく使う道具 #

Optional はもう使わない #

✗ 旧 / ✅ 新
# 旧
from typing import Optional
def find(id: int) -> Optional[str]: ...

# 新 — 常にこちら
def find(id: int) -> str | None: ...

Any は最後の手段 #

Any は「型チェックをオフにする」という宣言です。本当にわからないときだけ使い、object / Unknown (pyright) / unknown のような狭い型 に行けるかをまず検討してください。

Self — メソッドの戻り値で自身のクラス #

Self (3.11+)
from typing import Self

class Builder:
    def __init__(self):
        self.items: list[str] = []

    def add(self, item: str) -> Self:
        self.items.append(item)
        return self

-> Self は「このメソッドは呼ばれたクラスのインスタンスを返す」を意味します。ビルダーパターンによく合います。旧方式は -> "Builder" (forward reference) でしたが、サブクラスから呼ばれると正確ではありませんでした。Self がそれを解いてくれます。第20章 typing 上級 でもう少し詳しく見ます。

練習問題 #

  1. def last[T](items: list[T]) -> T | None: のシグネチャを持つ関数を書いてください。last([1, 2, 3])int | Nonelast(["a", "b"])str | None として推論されるかを pyright で確認します。
  2. ProtocolDrawable (def draw(self) -> str: ...) を定義し、def render_all(items: list[Drawable]) -> list[str]: を書いてください。継承なしに draw メソッドだけ持つクラスを2つ作って、render_all にそのインスタンスのリストを渡して正常に動作することを確認します。
  3. TypedDict + Literal で discriminated union を作ってください。ClickEvent (type: Literal["click"]x: inty: int) / KeyEvent (type: Literal["key"]code: str) の2型を合わせて Event = ClickEvent | KeyEvent にして、if event["type"] == "click": の分岐内で event["x"] のアクセスが型エラーなく動作することを確認します。

一行まとめ: Generic は def fn[T](...) / class Stack[T]: で型パラメータ化。Protocol は明示的な implements なしに形だけで約束 — collections.abc が標準集。TypedDict は dict の形を宣言して JSON / 外部データに適する (NotRequired / Required)。Literal は値を型に絞り込み、discriminated union の要。Optional は廃止、Any は最後の手段、Self はビルダーパターン。

次の章 #

次の 第10章 コンテキストマネージャ (with、contextlib) では、リソース管理の標準ツール — with 文と contextlib のあらゆるパターンを扱います。第6章 例外処理finally に書いた後片付けを、より安全で読みやすく表現する方法です。

X