Pythonの新ジェネリクス構文 PEP 695—type文とdef f[T]の書き方

Pythonの新ジェネリクス構文 PEP 695—type文とdef f[T]の書き方 | mohablog

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 のどちらか。混在やサブクラスは不可

サブクラスまで許すなら境界、特定の数種類に閉じたいなら制約。strbytes を別物として扱いたいケースは制約が向く。

型変数のスコープと変性の扱い

スコープが宣言場所に閉じる

旧来の 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) のように変性を手で指定していました。新構文では covariantcontravariant も書きません。型チェッカーが使われ方から変性を判定します。型理論の用語を前に出さずにジェネリッククラスを定義できる。

変性の指定漏れで型チェッカーに怒られる、という旧来のつまずきが減ります。読み手にとっても、宣言から余計なキーワードが消えて意図が追いやすくなる。

型パラメータにデフォルトを持たせる(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] なら、省略時の Tstr に落ちる。

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の記事でも扱っています。型パラメータの直書きと組み合わせると、型注釈の見通しがさらに良くなる。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次