Python3.12からPEP695-Type Parameter Syntax(型引数構文)が導入され、型変数を使ったクラスや関数の定義が大きく変わる

## 目次

- [はじめに](#はじめに)
- [PEP全文](#pep全文)
- [このPEPが提案された経緯](#このpepが提案された経緯)
- [「混乱のもと」とは何か](#混乱のもととは何か)
  - [型変数のスコープルールがわかりにくい](#型変数のスコープルールがわかりにくい)
  - [いつ`covariant`と`contravariant`を使う(使わない)べきかわかりにくい](#いつcovariantとcontravariantを使う使わないべきかわかりにくい)
  - [型引数の順番ルールがわかりにくい](#型引数の順番ルールがわかりにくい)
  - [グローバルに宣言しているので「すべての参照」に引っかかる](#グローバルに宣言しているのですべての参照に引っかかる)
  - [型変数の命名にはプライベート性と冗長さを意識しなければならない](#型変数の命名にはプライベート性と冗長さを意識しなければならない)
  - [`TypeVar`ライクや`Generic`ライクなシンボルを`typing`からインポートする必要がある](#typevarライクやgenericライクなシンボルをtypingからインポートする必要がある)
- [PEP695導入後にPythonの書き方はどう変わるか](#pep695導入後にpythonの書き方はどう変わるか)
  - [`TypeVar`シンボルの宣言を新たな構文に置き換えることができる](#typevarシンボルの宣言を新たな構文に置き換えることができる)
    - [関数の場合](#関数の場合)
    - [クラスの場合](#クラスの場合)
  - [`Protocol`を定義するための構文](#protocolを定義するための構文)
  - [`TypeVarTuple`や`ParamSpec`の宣言を代替する構文](#typevartupleやparamspecの宣言を代替する構文)
  - [ジェネリック型エイリアスを表現するための`type`文の登場](#ジェネリック型エイリアスを表現するためのtype文の登場)
  - [型の束縛(`TypeVar`の`bound`引数)相当をやりたい場合](#型の束縛typevarのbound引数相当をやりたい場合)
  - [型の制約(`TypeVar`の可変長引数相当)をやりたい場合](#型の制約typevarの可変長引数相当をやりたい場合)
  - [`TypeVar`のコンストラクタ引数に`infer_variance`が追加](#typevarのコンストラクタ引数にinfer_varianceが追加)
  - [`TypeVar`と新しい構文との互換性](#typevarと新しい構文との互換性)
  - [その他](#その他)
- [付録: ソフトキーワードの変化](#付録-ソフトキーワードの変化)
- [まとめ](#まとめ)

## はじめに
[Python3.12からPEP695で提案されたType Parameter Syntax(型引数構文)が導入され](https://docs.python.org/3.12/whatsnew/3.12.html#summary-release-highlights)、これまで`typing`モジュールから`Generic`や`TypeVar`などをインポートしてやるほかなかったジェネリクスや型変数の定義方法が大きく変わります。

これは実装にあたり新たな制御文を増やしているほどの大きな変更であり、3.12の目玉機能だと思うのですが、日本語で書かれている記事は(2023/07/04時点で)ありませんでした。

なので、ここに記事を書いてみることにしました。

## PEP全文

https://peps.python.org/pep-0695/

詳しくは上記の元ドキュメントを読んでください。
本記事では、上記PEPの要約を説明します。

## このPEPが提案された経緯
Python3.5で`typing`など型ヒントのための機能が導入([PEP484](https://peps.python.org/pep-0484/))されて以来、型ヒントはPythonの機能の中でも人気があるものの一つになりました。
型ヒント機能は[ジェネリクス](https://docs.python.org/ja/3/library/typing.html#generics)を用いることで表現力を豊かにすることができます。
しかし、それを行うには`typing`から`Generic`などをインポートしてそれを継承してジェネリクスを定義する必要がありました。
さらに、それらに渡す型引数のために`typing`から`TypeVar`/`ParamSpec`/`TypeVarTuple`をインポートして型変数を定義する必要もありました。
Pythonの開発者たちはこれらの機能をPythonの組み込み(bulit-in)機能ではなくネジ止め(bolt-on)機能であるように感じていて、混乱のもとになっていました。
そこで、他の現代的なプログラミング言語にあるのと同等の構文を用いて、ジェネリクスやパラメータ型を表現できるようにすべくこのPEPが提案されました。

## 「混乱のもと」とは何か
### 型変数のスコープルールがわかりにくい
型変数は通常グローバルスコープ内でアサインされます。
```py
_T = TypeVar("_T")
```

しかし、それを型引数として使う時、意味付けはジェネリッククラス、関数、型エイリアスの文脈内で使用される場合にのみ有効です。
そして型変数のインスタンスは複数の文脈で再利用される可能性があり、それぞれのコンテキストで異なる意味を持つことができてしまいます。

```py
# インスタンス属性`hoge`の型がなんであるかはコンストラクタ引数に
# 渡された`hoge`引数の型で決まることを型チェッカーに伝える
class Foo(Generic[_T]):
    hoge: _T

    def __init__(self, hoge: _T) -> None:
        ...


# この関数が返す型がなんであるかは引数に渡されたシーケンスの型引数が
# 何型かで決まることを型チェッカーに伝える
def get_first(seq: Sequence[_T]) -> _T:
    return seq[0]
```

上記のコードでは、グローバルに宣言した`_T`をまったく違う意味として使えてしまっています。
つまり、ひとつの型変数に過剰な責任が載せられてしまっています。
もちろん、別々の名前で宣言することで、意味が違うことを強調し、責任を分散させることもできます。
しかし、ジェネリッククラス、関数、型エイリアスを定義しようとするたびに新しい名前を付けていくとそれはそれで混乱を招くし、コーダーが思いつく名前はいずれ枯渇するでしょう。

### いつ`covariant`と`contravariant`を使う(使わない)べきかわかりにくい
[PEP483](https://peps.python.org/pep-0483)と[PEP484](https://peps.python.org/pep-0484)では型変数の「変性」という概念が導入されました。
型変数のコンストラクタ引数で
- `covariant=True`ならば「共変」
- `contravariant=True`ならば「反変」
- それらでなければ「不変」(invariance)

であることが指定できます。
しかし、これは非常に高度な概念[^1] [^2]で、ほとんどのPython開発者に理解されていません。

### 型引数の順番ルールがわかりにくい
ジェネリッククラスや型エイリアスで複数の型引数を使用する場合、`Generic`(や`Protocol`)を多重継承することで上書きすることができます。例えば`class ClassA(Mapping[K, V])`では型引数は`K`、`V`の順に並びますが、`class ClassB(Mapping[K, V], Generic[V, K])`では`V`、`K`の順に並びます。
この並び順の違いにどのような意味を持たせるかはクラス定義の詳細によりますが、後々になってコードを読んだ人が「入れ替えていることに何か意味があるのか?」と混乱するということは十分にありえるでしょう。

### グローバルに宣言しているので「すべての参照」に引っかかる
最近のIDEは、言語サーバーが解釈した意味レベルで同一の変数を操作する「すべての参照を検索」や「すべての参照の名前を変更」といった機能を提供しています。
しかし前述のように、グローバルに宣言された型変数をまったく違う文脈で使用していることも多いです。
IDEの機能でまったく違う文脈のシンボルを操作して、意味が通らない名前になってしまうかもしれません。

### 型変数の命名にはプライベート性と冗長さを意識しなければならない
型変数はたいてい、モジュールのプライベート変数(外部から参照されない)として定義されます。
プライベートであるため、[PEP8で推奨される「アンダースコアで始まる名前を付ける」](https://pep8-ja.readthedocs.io/ja/latest/#section-32)[^3]ルールが適用されます。
なので、`_T_contra`や`_KT_co`のような冗長な名前になってしまいます。また、現在の型変数はコンストラクタ引数で型変数名と等しい文字列を指定(ex. `_T = TypeVar("_T")`）しなければならず、これもまた冗長です。

### `TypeVar`ライクや`Generic`ライクなシンボルを`typing`からインポートする必要がある
ジェネリクスや型引数の概念は`typing`に関連して導入され、使用するには`typing`をインポートする必要がありました。
静的型付け機能が大人気になったので、Pythonの最近のリリースでは[PEP585](https://peps.python.org/pep-0585/)のような特別なシンボルをインポートする必要性をなくす努力がなされていましたが、`TypeVar`や`Generic`については後回しにされてきました。
同じ種類の概念を扱っていたのに、片方はインポートが必要で片方は不要なのは、「どっちがどっちだっけ」問題を引き起こして混乱のもとになります。

## PEP695導入後にPythonの書き方はどう変わるか

### `TypeVar`シンボルの宣言を新たな構文に置き換えることができる

下記に紹介する新しい構文では、`TypeVar`を使って型引数や型アノテーションに使う型変数をグローバルに宣言する必要がなくなります。
そして、型変数のスコープは宣言された文脈に限定されるようになり、冗長な命名や使いまわし問題を回避することが可能になります。

#### 関数の場合

`def 関数名[型引数](引数: 引数アノテーション) -> 返り値型アノテーション:`と書くことができるようになります。

```py
from typing import TypeVar

_T = TypeVar("_T")

def func(a: _T, b: _T) -> _T:
    ...
```
↓
```py
def func[T](a: T, b: T) -> T:
    ...
```

#### クラスの場合

`class クラス名[型引数](基底クラス):`と書くことができるようになります。

```py
_T = TypeVar("_T")

class ClassA(Generic[_T]):
    def method1(self) -> _T:
        ...

class ClassB(Iterator[_T]):
    def method1(self) -> _T:
        ...
```
↓
```py
class ClassA[T]:
    def method1(self) -> T:
        ...

class ClassB[T](Iterator):
    def method1(self) -> T:
        ...
```

旧来の`Generic`を使う方法と新しい構文を使ったジェネリッククラスの定義を併用することはできません。
```py
class ClassA[T](Generic[T]): ...  # 実行時エラー
```

### `Protocol`を定義するための構文
構造的部分型でダックタイピングを行う時に便利な`Protocol`ですが、`Generic`とは違ってこれは新しい構文でも個別にインポートして宣言する必要があります。
```py
from typing import Protocol

_S = TypeVar("_S")
_T = TypeVar("_T")

class ClassA(Protocol[_S, _T]): ...
```
↓
```py
from typing import Protocol

class ClassA[S, T](Protocol): ... # OK
```


### `TypeVarTuple`や`ParamSpec`の宣言を代替する構文
型引数シンボルの前に`*`をつけると`TypeVarTuple`、`**`をつけると`ParamSpec`として解釈します。

```py
_T = TypeVar("_T")
_Ts = TypeVarTuple("_Ts")
_P = ParamSpec("_P")
class Foo(Generic[_T, _Ts, _P]): ...
```
↓
```py
# シンボル`T`を`TypeVar`とするなら`T`と宣言する
# シンボル`Ts`を`TypeVarTuple`とするなら`*Ts`と宣言する
# シンボル`P`を`ParamSpec`とするなら`**P`と宣言する
class Foo[T, *Ts, **P]: ...
```

同じシンボルの型変数を同時に`TypeVar`/`TypeVarTuple`/`ParamSpec`として扱おうとすると`SyntaxError`となります。
```py
class ClassA[T, *T]: ... # SyntaxError

def func1[T, **T](): ... # SyntaxError
```

### ジェネリック型エイリアスを表現するための`type`文の登場

ジェネリック型エイリアスを定義するためには、型変数を定義するために必要な`TypeVar`に加えて([PEP613](https://peps.python.org/pep-0613/)で導入された)`TypeAlias`を使うことが推奨されていました。
```py
from typing import TypeAlias

_T = TypeVar("_T")

ListOrSet: TypeAlias = list[_T] | set[_T]
```

`TypeVar`、`TypeAlias`を使わずに型エイリアスを明示的に宣言できるように、新たに`type`文が導入され、上記のコードは下記のように書き直すことができるようになります。
```py
type ListOrSet[T] = list[T] | set[T]
```

### 型の束縛(`TypeVar`の`bound`引数)相当をやりたい場合
```py
_T_D = TypeVar("_T_D", bound=dict[str, int])

class ClassA(Generic[_T_D]): ...  # OK

_T_FR = TypeVar("_T_FR", bound="ForwardReference")

class ClassB(Generic[_T_FR]): ...  # OK
```
↓
```py
class ClassA[T: dict[str, int]]: ...  # OK

class ClassB[T: "ForwardReference"]: ...  # OK
```
下記のような書き方はエラーになります。
```py
class ClassC[V]:
    class ClassD[T: dict[str, V]]: ...  # 型チェッカーエラー、このやり方でジェネリックを指定できない

class ClassE[T: [str, int]]: ...  # 型チェッカーエラー、この書き方は許されない
```

### 型の制約(`TypeVar`の可変長引数相当)をやりたい場合
```py
_T_A = TypeVar("_T_A", str, bytes)

class ClassA(Generic[_T_A]): ...

_T_B = TypeVar("_T_B", "ForwardReference", bytes)

class ClassB(Generic[_T_B]): ...
```
↓
```py
class ClassA[T: (str, bytes)]: ...  # OK

class ClassB[T: ("ForwardReference", bytes)]: ...  # OK
```
下記のような書き方はエラーになります。
```py
class ClassC[T: ()]: ...  # 型チェッカーエラー、長さ2以上のタプルとして指定必須

class ClassD[T: (str, )]: ...  # 型チェッカーエラー、長さ2以上のタプルとして指定必須

t1 = (bytes, str)
class ClassE[T: t1]: ...  # 型チェッカーエラー、リテラルタプルとして指定必須
```

### `TypeVar`のコンストラクタ引数に`infer_variance`が追加

`infer_variance`が`TypeVar`のオプショナルなコンストラクタ引数として追加され、`True`を指定すると変性は型チェッカーが推論するようになります。
[従来の`TypeVar`は`infer_variance=False`がデフォルト](https://docs.python.org/ja/3.12/library/typing.html#typing.TypeVar)であるため、(他のオプショナル引数を指定しなければ)不変として解釈されることは変わりません。

以前に不変である型変数を使いたくない場面がある場合、変性について熟知していなければなりませんでした。

```py
from typing import Generic, TypeVar

_T_co = TypeVar("_T_co", covariant=True, bound=str)

class ClassA(Generic[_T_co]):
    def method1(self) -> _T_co:
        ...
```

上記のコードは下記のように書くことができるようになり、変性の指定について悩む必要がなくなります。

```py
from typing import Generic, TypeVar

_T = TypeVar("_T", infer_variance=True, bound=str)

class ClassA(Generic[_T]):
    def method1(self) -> _T:
        ...
```

実は、これまで紹介した新しい構文で型変数を宣言した際には、変性を(指定できず)型チェッカーの推論に任せることになります。
つまり、`TypeVar(..., infer_variance=True, ...)`である型変数を用いるのと同じことになっています。

上記のコードは下記のように書き直すことができます。

```py
class ClassA[T: str]:
    def method1(self) -> T:
        ...
```


### `TypeVar`と新しい構文との互換性
新しい構文とは、旧来の`TypeVar`と併用/互換して使える実装がされています。

```py
K = TypeVar("K")

class ClassA[V](dict[K, V]): ...  # NG、`ClassA(dict[K, V], Generic[V])`ができないのと同じ理由

class ClassB[K, V](dict[K, V]): ...  # OK、`ClassA(dict[K, V], Generic[K, V])`と同じ

class ClassC[V]:
    # `method1`で使用されている`K`と`V`は型引数が「暗黙的」で「伝統的」なジェネリック関数の
    # メカニズムを使用しているため問題ない。この場合、
    # - `V`は`ClassC`で宣言されている型引数から
    # - `K`はグローバルで宣言されている`TypeVar`から
    # 「暗黙的」に導入されている。
    def method1(self, a: V, b: K) -> V | K: ...  # OK

    # `method2`で`M`と`K`は使えない。メソッドの型引数を新しい構文で宣言する場合は、
    # すべての型引数を新しい構文で「明示的」に宣言する必要がある。
    def method2[M](self, a: M, b: K) -> M | K: ...  # NG
```

### その他
実行時にクラスやインスタンスに付与されるdunder属性/メソッドやデコレータ、`nonlocal`との相互作用などがPEP695には記載されています。

## 付録: ソフトキーワードの変化
Pythonでは`match`のように「特定の文脈だけ制御文」「そうでなければ普通の変数名としてその単語で宣言可能」「よって、制御構文の単語と変数名の単語が一緒でも問題ない」という[「特定の文脈でのみ予約される識別子」](https://docs.python.org/ja/3.10/reference/lexical_analysis.html#soft-keywords)を「ソフトキーワード」という概念にしています。
Python3.9で`keyword`モジュールには[空リスト`softkwlist`及び`False`のみを返す関数`issoftkeyword`が導入](https://github.com/python/typeshed/blob/7544b60db7b08249b1fef3663f0a68736660cae8/stdlib/keyword.pyi)されました。
`match`文が導入されたPython3.10ではソフトキーワードリストに[`match`、`case`、`_`が追加](https://docs.python.org/ja/3.10/reference/lexical_analysis.html#soft-keywords)されました。
Python3.12では型エイリアスを`typing.TypeAlias`なしで明示することができるように、ソフトキーワードリストに[`type`が追加](https://docs.python.org/ja/3.12/reference/lexical_analysis.html#soft-keywords)されました。

## まとめ
これまでもPythonの静的型付けシステムには様々な機能が付け加えられました。
- `typing`からコレクション型エイリアスをインポートしなくてもビルトイン型で型アノテーションができるようになる([PEP585](https://peps.python.org/pep-0585/))
- `ParamSpec`を使うことでコールバック関数の引数に制約を持たせることが可能になる([PEP614](https://peps.python.org/pep-0612/))
- `TypeVarTuple`を使うことで可変長ジェネリクスが可能になる([PEP646](https://peps.python.org/pep-0646/))

型アノテーションの重要性について論じた[『ロバストPython』](https://www.oreilly.co.jp/books/9784814400171/)も刊行され、従来のPythonエンジニアにとって縁遠いものだった型アノテーションについて認知が高まっています。
PEP695はこれまでの静的型付け機能の中でも、クラスや関数の定義のための新たな構文が追加され、新たな文(`type`)が増えるという大きな変更です。
今後も、コミュニティの拡大によって、新たな機能が静的型付けシステムに付け加えられていくことでしょう。
個人的には、動的ミックスインやファクトリ関数のアノテーションが簡単にできるようになる[交差型`Intersection`の導入](https://github.com/python/typing/issues/213)を楽しみにしています。


[^1]: 筆者も「説明しろ」と言われるとかなり言葉につまります。『ロバストPython』でも`TypeVar`のオプショナル引数の意味について説明していません。もしやるとすればTypeScriptで同等の概念を扱うので、それを引用しながら説明することになると思います。
[^2]:  反変や共変であることを指定するべきかどうかは型チェッカーによって異なります。筆者は`pylance`が「ここは反変にすべき」という警告を発して`mypy`が警告を発しない場面を目にしたことがあります。
[^3]: Pythonの型スタブリポジトリ[`typeshed`の`CONTRIBUTING.md`](https://github.com/python/typeshed/blob/main/CONTRIBUTING.md#conventions)には型変数名をアンダースコアはじまりにすべきと明記されています。