Pythonのジェネリック関数は、これまで TypeVar をモジュール先頭で宣言してから書いていました。Python 3.12 の PEP 695 で、その前置きが消えます。実行結果はすべて現行の Python 3.14.6 のもの。
type文で型エイリアスを定義する
TypeAliasアノテーションからtype文へ
型エイリアスは type 文で書きます。旧来の TypeAlias アノテーションは不要。
from typing import TypeAlias
Vector: TypeAlias = list[float]
これが新構文ではこうなります。
type Vector = list[float]
print(type(Vector).__name__)
print(Vector.__value__)
実行結果。
TypeAliasType
list[float]
type 文が作るのは TypeAliasType のインスタンスです。What’s New 3.12 の “PEP 695: Type Parameter Syntax” にも、この文が TypeAliasType を生成すると書かれています。右辺は __value__ から取れる。
ジェネリックな型エイリアス
角かっこで型パラメータを付ければ、エイリアス自体がジェネリックになります。
type Pair[T] = tuple[T, T]
print(Pair.__type_params__)
print(Pair[int].__value__)
出力はこう。
(T,)
tuple[T, T]
__type_params__ に T が入ります。Pair[int] と具体型を渡しても、__value__ は型パラメータの形のまま保持される。
遅延評価で前方参照を書く
右辺は遅延評価です。PEP 695 の “Lazy evaluation” にある通り、型エイリアスの右辺や境界・制約はコードオブジェクトとして保存され、参照された時点で評価されます。だから再帰的な型もクォートなしで書ける。
type Tree[T] = T | list[Tree[T]]
print(Tree.__value__)
結果。
T | list[Tree[T]]
旧来なら "Tree[T]" と文字列で囲む必要がありました。遅延評価でその手間が消える。
関数とクラスに型パラメータを直接書く
ジェネリック関数
関数名の直後に [T] を置くだけ。importもグローバル宣言も書きません。
def first[T](xs: list[T]) -> T:
return xs[0]
print(first.__type_params__)
print(first([10, 20, 30]))
実行結果。
(T,)
10
ジェネリッククラス
クラスも同じ。Generic[T] の継承が消えます。まず旧構文。
from typing import TypeVar, Generic
T = TypeVar("T")
class Box(Generic[T]):
def __init__(self, value: T) -> None:
self.value = value
新構文だと宣言が1行に収まります。
class Box[T]:
def __init__(self, value: T) -> None:
self.value = value
print(Box.__type_params__)
出力。
(T,)
__type_params__は新構文だけが持つ
差が出るのは __type_params__。旧来の Generic[T] 継承では、この属性が空のタプルになります。
from typing import TypeVar, Generic
T = TypeVar("T")
class OldBox(Generic[T]):
pass
print(OldBox.__type_params__)
返ってくる値。
()
新構文のクラスや関数だけが、型パラメータを属性として保持します。リフレクションで型パラメータを拾う処理を書くなら、旧構造との差は無視できない。型ヒントの基礎はPythonの型ヒントとmypyの記事にまとめています。
境界と制約で受け取る型を絞る
上限境界(bound)
[T: Base] で上限を付けます。Base とそのサブクラスだけを受け取る。
from collections.abc import Hashable
def longest[T: Hashable](a: T, b: T) -> T:
return a
print(longest.__type_params__[0].__bound__)
実行結果。
<class 'collections.abc.Hashable'>
制約(constraints)
かっこで型を並べると制約になります。並べた型のどれか1つに固定される。
def pick[T: (int, str)](x: T) -> T:
return x
print(pick.__type_params__[0].__constraints__)
出力はこう。
(<class 'int'>, <class 'str'>)
boundとconstraintsの早見表
両者は似ていますが、受け取れる型が違います。
| 書き方 | 意味 | T に入る型 |
|---|---|---|
[T: Base] | 上限境界 | Base とそのサブクラス |
[T: (A, B)] | 制約 | A か B のどちらか。混在やサブクラスは不可 |
サブクラスまで許すなら境界、特定の数種類に閉じたいなら制約。str と bytes を別物として扱いたいケースは制約が向く。
型変数のスコープと変性の扱い
スコープが宣言場所に閉じる
旧来の T = TypeVar("T") はモジュールのグローバルに残り、複数の用途で使い回されてスコープが追いにくくなっていました。PEP 695 の “Motivation” でも、型変数がグローバルに割り当てられる点を課題として挙げています。新構文の T は、宣言した関数やクラスの内側にしか存在しません。外で触ると NameError。
def g[T](x: T) -> T:
return x
print(T)
実行結果。
NameError: name 'T' is not defined
変性は型チェッカーが推論する
旧来は TypeVar("T", covariant=True) のように変性を手で指定していました。新構文では covariant も contravariant も書きません。型チェッカーが使われ方から変性を判定します。型理論の用語を前に出さずにジェネリッククラスを定義できる。
変性の指定漏れで型チェッカーに怒られる、という旧来のつまずきが減ります。読み手にとっても、宣言から余計なキーワードが消えて意図が追いやすくなる。
型パラメータにデフォルトを持たせる(Python 3.13)
=でデフォルトを書く
Python 3.13 の PEP 696 で、型パラメータにデフォルトが付きます。[T = dict] のように = で指定する。
class Repo[T = dict]:
def __init__(self) -> None:
self.items: list[T] = []
tp = Repo.__type_params__[0]
print(tp.has_default())
print(tp.__default__)
実行結果。
True
<class 'dict'>
型エイリアスでも同じ書き方が通ります。type Response[T = str] = dict[str, T] なら、省略時の T は str に落ちる。
3.12では構文エラーになる
デフォルトは 3.13 以降の機能です。3.12 で書くと実行前に弾かれます。
python3.12 -c "class C[T = int]: pass"
返ってくるエラー。
SyntaxError: invalid syntax
型パラメータの直書き自体は 3.12 から使えますが、デフォルトだけは 3.13 の線引き。対象ランタイムを確かめてから入れてください。
旧コードはRuffでまとめて移行する
UP040・UP046・UP047で検出する
手で書き換えなくても、Ruff が旧構文を見つけて新構文へ直します。pyupgrade系のルールが該当する。型エイリアスは UP040、ジェネリッククラスは UP046、ジェネリック関数は UP047。次の旧構文ファイルを例にします。
from typing import TypeVar, Generic, TypeAlias
T = TypeVar("T")
Vector: TypeAlias = list[float]
def first(xs: list[T]) -> T:
return xs[0]
class Box(Generic[T]):
def __init__(self, value: T) -> None:
self.value = value
検出だけ走らせます。
ruff check --select UP040,UP046,UP047 --target-version py312 old_style.py
実行結果(抜粋)。
UP040 Type alias `Vector` uses `TypeAlias` annotation instead of the `type` keyword
UP047 Generic function `first` should use type parameters
UP046 Generic class `Box` uses `Generic` subclass instead of type parameters
Found 3 errors.
–fixで一括変換する
変換は --unsafe-fixes 側に入っています。意味が変わらないか確認してから当てる前提のフラグ。
ruff check --select UP040,UP046,UP047 --fix --unsafe-fixes old_style.py
変換後のコード。
type Vector = list[float]
def first[T](xs: list[T]) -> T:
return xs[0]
class Box[T]:
def __init__(self, value: T) -> None:
self.value = value
3ファイルではなく3箇所が新構文に揃いました。変換後のコードは 3.12 でもそのまま動きます。
変換後に残る後始末
ただし移行後に残る手作業が1つ。手元の旧構文プロジェクトに --fix --unsafe-fixes をかけたとき、構文は新しくなったのに T = TypeVar("T") の行と from typing import TypeVar, Generic, TypeAlias がそのまま残りました。Ruffが直すのは構文で、不要になった宣言とimportの削除は別ルール(未使用import検出の F401 など)の担当だからです。移行は UP04x の変換、続けて未使用宣言とimportの除去、の2段で進める。Ruffの設定はRuff入門の記事に書いています。
よくある質問
旧構文と新構文は混在できる?
ファイル単位なら混在できます。新構文のクラスと、旧 TypeVar ベースのクラスは同じモジュールに共存する。ただし1つのクラス定義で両方を混ぜると弾かれます。
from typing import Generic, TypeVar
U = TypeVar("U")
class C[T](Generic[U]):
pass
実行結果。
TypeError: Cannot inherit from Generic[...] multiple times.
新構文の [T] がすでにそのクラスをジェネリックにするため、Generic の明示は二重指定になります。どちらか片方に揃える。
実行時に型はチェックされる?
されません。型パラメータは型チェッカーとエディタ補完のための情報で、実行時に値の型を検査しない。list[int] を期待する関数に文字列のリストを渡しても、そのまま動きます。
def first[T](xs: list[T]) -> T:
return xs[0]
print(first(["a", "b"]))
出力。
a
型の不整合は mypy などの静的解析で拾う。実行時の防御が要るなら、別途バリデーションを入れる。
まとめ
PEP 695 と PEP 696 を、現行の Python 3.14.6 で確かめた結果をまとめます。
type文で型エイリアスを定義。TypeAliasアノテーションは不要で、右辺は__value__から取れる(Python 3.12)- 関数は
def f[T]、クラスはclass C[T]で型パラメータを直書き。TypeVarのグローバル宣言が消える - 上限は
[T: Base]、制約は[T: (A, B)]。変性は型チェッカーが推論する - 新構文のクラスと関数は
__type_params__を持つ。旧Generic[T]では空のタプル - デフォルト型パラメータ
[T = ...]は Python 3.13 の PEP 696 から。3.12 ではSyntaxError - 旧コードは Ruff の
UP040/UP046/UP047で移行。未使用宣言の除去は別の段で行う
構造的部分型でのDI設計はProtocolの記事でも扱っています。型パラメータの直書きと組み合わせると、型注釈の見通しがさらに良くなる。

![Pythonの新ジェネリクス構文 PEP 695—type文とdef f[T]の書き方 | mohablog](https://mohablog.com/wp-content/uploads/2026/06/thumbnail-1782818067.webp)