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章 変数、基本型と型ヒント で次まで押さえました。
int、str、bool、Nonelist[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 なのでエディタがその情報を失います。パラメータ化が必要です。
def first[T](items: list[T]) -> T:
return items[0]
x = first([1, 2, 3]) # x: int
y = first(["a", "b"]) # y: strdef first[T] の形が Python 3.12 の新文法です。PEP 695。以前は次のように書きました。
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]新コードは メソッド / 関数の横の [T] の位置に直接書く方式 で統一すればよいです。
複数の型パラメータ #
def pair[K, V](key: K, value: V) -> tuple[K, V]:
return (key, value)
p = pair("name", 42) # tuple[str, int]型制約 — 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 の方が合います。(下で扱う)
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 #
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 | NoneStack[int] のように 使用時に T を埋めて 使うのがジェネリッククラスの使い方です。Variance(共変・反変・不変)のような話題は第20章 typing 上級 で扱います。
Protocol — ダックタイピングを型に #
Python の慣用句は 「どのクラスのインスタンスか」ではなく「どのメソッド / 属性を持つか」 です。これを静的型で表現するのが Protocol です。
Java / C# のインターフェースではない #
Java のインターフェースは 明示的に implements を書かないと そのインターフェースになりません。Python の Protocol はそれが必要ありません。形さえ合えば その Protocol を満たします (structural typing)。
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 が役立つ場面 — 「必要な分だけ約束」 #
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 がすでにあります。
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 で扱うとき、どのキーとどの値の型 が入るかを書きたいとき。
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 との違い #
| dataclass | TypedDict | |
|---|---|---|
| ランタイムの形 | ユーザー定義クラスのインスタンス | 通常の dict |
| メソッド | __init__、__repr__、__eq__ 自動 | なし (dict のメソッドのみ) |
| アクセス | u.name | u["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:
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]AdminUser は id、name、permissions の3キーを持ちます。
Literal — 狭い union #
値そのものを型に。特定の文字列 / 数字だけ許可したいとき。
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 #
type Color = Literal["red", "green", "blue"]
type Mode = Literal["light", "dark", "auto"]
def render(color: Color, mode: Mode) -> str:
...定数と組み合わせ — Final
#
from typing import Final, Literal
DEFAULT_LEVEL: Final = "info"
# DEFAULT_LEVEL の型が Literal["info"] に絞られる
DEFAULT_LEVEL = "warn" # ✗ Final — 再代入不可Final は「この変数は一度だけ代入」を表します。モジュール定数によく使います。
Discriminated union — 決定的な分岐 #
異なる形の dict が一つの場所に入ってくるとき、「どの形か」を一つのキーで識別するパターン。
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 — メソッドの戻り値で自身のクラス
#
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 上級 でもう少し詳しく見ます。
練習問題 #
def last[T](items: list[T]) -> T | None:のシグネチャを持つ関数を書いてください。last([1, 2, 3])がint | None、last(["a", "b"])がstr | Noneとして推論されるかを pyright で確認します。ProtocolでDrawable(def draw(self) -> str: ...) を定義し、def render_all(items: list[Drawable]) -> list[str]:を書いてください。継承なしにdrawメソッドだけ持つクラスを2つ作って、render_allにそのインスタンスのリストを渡して正常に動作することを確認します。TypedDict+Literalで discriminated union を作ってください。ClickEvent(type: Literal["click"]、x: int、y: 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 に書いた後片付けを、より安全で読みやすく表現する方法です。