Python3.11～3.13で追加された主な型表現まとめ

### 3.10までのまとめ ↓

https://qiita.com/nicco_mirai/items/5591c1c48b1422c324c9

知識のアップデートやっていきます。

## 3.11
### 可変長ジェネリクス
複数の値を持つことができる型変数[**`TypeVarTuple`**](https://docs.python.org/ja/3.14/library/typing.html#typing.TypeVarTuple)が追加されました。
簡単な例をあげると…

```py
from typing import TypeVarTuple

Ts = TypeVarTuple("Ts")

def variadic_generics(*args: *Ts) -> tuple[*Ts]:
    return args
```

のような関数を定義した場合、

```py
ret1 = variadic_generics(1, "hoge")
ret2 = variadic_generics(2, "fuga", False)
```

`ret1`は`tuple[int, str]`に、`ret2`は`tuple[int, str, bool]`にそれぞれ正しく推論されますよという感じです。

**が、この構文は3.12で早速使わなくなりました。** → [後述](https://qiita.com/nicco_mirai/items/75e2fccd3afbdfb64eb4#%E5%9E%8B%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E6%A7%8B%E6%96%87)

### 一部のキーが省略可能なTypedDict

厳格な型の`dict`がほしい場合に用いられる[**`TypedDict`**](https://docs.python.org/ja/3.14/library/typing.html#typing.TypedDict)は従来全てのキーが存在することを強制されていましたが、3.11からは省略可能なキーを設定することができるようになりました。

```py:従来
from typing import TypedDict

class Movie(TypedDict):
   title: str
   year: int

m1: Movie = {"title": "Black Panther", "year": 2018}  # OK
m2: Movie = {"title": "Star Wars"}                    # Error
```

```py:yearはなくてもいい場合
from typing import TypedDict, NotRequired

class Movie(TypedDict):
   title: str
   year: NotRequired[int]

m1: Movie = {"title": "Black Panther", "year": 2018}  # OK
m2: Movie = {"title": "Star Wars"}                    # OK！
```

とは言えデータクラスで済む場合は`@dataclass`で問題ありません。

```py: データクラス版
@dataclass
class Movie:
   title: str
   year: int | None = None

m1 = Movie("Black Panther", 2018)  # OK
m2 = Movie("Star Wars")            # OK
```

あくまでも`dict`がほしい場合にのみ使用しましょう。

### Self型
代替コンストラクタなど自分自身を返すメソッドの戻り値に雑にアノテーションをつけられなかったもやもやが解消されました。

```py
from dataclasses import dataclass
from typing import Self

@dataclass
class MyInt:
    val: int

    @classmethod
    def fromhex(cls, s: str) -> Self:
        return cls(int(s, 16))
```

ちなみに通常メソッドの第一引数`self`は`Self`、クラスメソッドの`cls`は`type[Self]`型ですがそこまでするかどうかはプロジェクト次第ですね。私はやらないと思います[^cls]。

### 文字列リテラル型

セキュリティの観点などからリテラル**ではない**文字列を弾きたいような場面で使える[**`LiteralString`**](https://docs.python.org/ja/3.14/library/typing.html#typing.LiteralString)型が追加されました。

```py
from typing import LiteralString

def run_query(sql: LiteralString) -> ...:
    ...

run_query("SELECT * FROM students")  # OK
run_query(input())                   # Error
run_query(                           # f文字列もstr型のためError
    f"SELECT * FROM students WHERE name = {input()}"
)
```

### データクラスへの変換

データクラスのようなインターフェースを持つサードパーティ製ライブラリのクラスをデータクラスと同じように型検査できるようにするためのデコレータ[**`@dataclass_transform()`**](https://docs.python.org/3.14/library/typing.html#typing.dataclass_transform)が追加されました。

そもそものペインなど詳しくは ↓

https://www.python.jp/news/wnpython311/typing3.html

### ボトム型

従来Pythonでは`NoReturn`を転用してボトム型を表現していましたが、正式に[**`Never`**](https://docs.python.org/3.14/library/typing.html#typing.Never)型が使えるようになりました。

```py
from typing import Never

def never_call_me(arg: Never) -> None:
    pass

def int_or_str(arg: int | str) -> None:
    match arg:
        case int():
            print("It's an int")
        case str():
            print("It's a str")
        case _:
            never_call_me(arg)  # OK
```


## 3.12

### 型パラメータ構文

3.12からは**型エイリアスの書き方**と**ジェネリクスの書き方**が大きく変わります。

まず[**`type`文**](https://docs.python.org/ja/3.14/reference/simple_stmts.html#type)が追加されたことにより型エイリアスがこのように書けるようになりました。

```py
type Point = tuple[float, float]
```

更にこれをジェネリックにしたい場合はこうです。

```py
type Point[T] = tuple[T, T]
```

一番のポイントは**型変数を事前に定義しなくてもよい**ことで、これによって`TypeVar`や`TypeVarTuple`、`ParamSpec`などをインポートする必要がなくなりました。

```py: 用例
type IntFunc[**P] = Callable[P, int]                # ParamSpec
type LabeledTuple[*Ts] = tuple[str, *Ts]            # TypeVarTuple
type HashableSequence[T: Hashable] = Sequence[T]    # TypeVar with bound
type IntOrStrSequence[T: (int, str)] = Sequence[T]  # TypeVar with constraints
```

関数やクラスについても同様の書き方でジェネリックにすることが可能で、特にクラスについては明示的に`Generic`を継承する必要もなくなりました。

```py: やったぜ
from typing import Iterable

def max[T](args: Iterable[T]) -> T:
    ...

class MyList[T]:
    def __getitem__(self, index: int, /) -> T:
        ...

    def append(self, element: T) -> None:
        ...
```

なお`type`はソフトキーワード(特定の文脈でのみ予約語)だそうです。

### 可変長キーワード引数の型付け

```py
def print_(**kwargs: str):
    print(kwargs)

print_(hoge="hoge", fuga="fuga")  # OK
print_(hoge="hoge", piyo=1)       # `piyo`の値がstr型ではないためError
```

このように可変長キーワード引数は全ての値が同一の型でなければ正しくアノテーションすることができないので何とかしたいと。

そこまでするなら素直に構造体を受け取ればいいのに… と思い[**PEP**](https://peps.python.org/pep-0692/#motivation)を見にいったところ

> As described in the Intended Usage section, using **kwargs is not always the best tool for the job. Despite that, it is still a widely used pattern. As a consequence, there has been a lot of discussion around supporting more precise **kwargs typing and it became a feature that would be valuable for a large part of the Python community. This is best illustrated by the mypy GitHub issue 4441 which contains a lot of real world cases that could benefit from this propsal.

> 要約: `**kwargs`を使うことがベストじゃないのは分かってるけどむっちゃ使われちゃってるししょうがない

などと書かれており悲しい気持ちになりました。紹介は割愛します。

### 明示的オーバーライド

[**`@override`**](https://docs.python.org/ja/3.14/library/typing.html#typing.override)デコレータが追加され、メソッドのオーバーライドを明示できるようになりました。

```py:
from typing import override

class Base:
    def log_status(self) -> None:
        ...

class Sub(Base):
    @override
    def log_statas(self) -> None:  # typoによりオーバーライドできてないためError
        ...
```

うっかりを防ぐ意味でも使った方がいいと思います。

## 3.13

### デフォルト型引数

ジェネリクスにデフォルトの型を指定できるようになりました。

https://typescriptbook.jp/reference/generics/default-type-parameter

```py:
from dataclasses import dataclass
from typing import TypeVar

ET = TypeVar("ET", bound=Exception, default=SyntaxError)

@dataclass
class MyErrorEvent[ET]:
  error: ET
  type: str
```

ただし現在サポートされているのは`TypeVar` / `TypeVarTuple` / `ParamSpec`の引数に直接渡してあげる方法のみで、

```py:
from dataclasses import dataclass

@dataclass
class MyErrorEvent[T: Exception = SyntaxError]:
  error: T
  type: str
```

このようにはまだ書けないようです。

### 一部の値が読み取り専用なTypedDict

```py:
from typing import TypedDict, ReadOnly

class Movie(TypedDict):
   title: ReadOnly[str]
   year: int

def mutate_movie(m: Movie) -> None:
   m["year"] = 1999           # OK
   m["title"] = "The Matrix"  # Error
```

`TypedDict`の機能が拡張されまくっているのはどういう理由からなんでしょうか？一部だけReadOnlyにしたいという需要もよく分かりませんし普通に`@dataclass`でイミュータブルなオブジェクトを作ればいいだけのように思います。

```py:
from dataclasses import dataclass

# `frozen`を指定することで各値が変更不可になる
@dataclass(frozen=True)
class Movie:
   title: str
   year: int

def mutate_movie(m: Movie) -> None:
   m.year = 1999           # Error
   m.title = "The Matrix"  # Error
```

### 賢くなったユーザー定義型ガード

従来の`TypeGuard`は元々の型情報が考慮されないため、`isinstance()`のような分岐を書こうとしてもうまく型を絞り込めないということがよくありました。

```py: isinstanceを使った場合
def typeguard(x: str | int) -> None:
    if isinstance(x, str):
        reveal_type(x)  # str
    else:
        reveal_type(x)  # int
```

```py: TypeGuardを使った場合
from typing import TypeGuard

def is_str(x: object) -> TypeGuard[str]:
    return isinstance(x, str)

def typeguard(x: str | int) -> None:
    if is_str(x):
        reveal_type(x)  # str
    else:
        reveal_type(x)  # str | int ← ？
```

新しく追加された[**`TypeIs`**](https://docs.python.org/ja/3.14/library/typing.html#typing.TypeIs)ではこの点が解消され、`isinstance()`などと同じように型を絞り込むことが可能になっています。

```py: TypeIsを使った場合
from typing import TypeIs

def is_str(x: object) -> TypeIs[str]:
    return isinstance(x, str)

def typeguard(x: str | int) -> None:
    if is_str(x):
        reveal_type(x)  # str
    else:
        reveal_type(x)  # int ← ！
```

もう少し詳しい挙動 ↓

```py
from typing import TypeGuard, TypeIs, final, reveal_type

class Base: ...
class Child(Base): ...
@final
class Unrelated: ...

def is_base_typeguard(x: object) -> TypeGuard[Base]:
    return isinstance(x, Base)

def is_base_typeis(x: object) -> TypeIs[Base]:
    return isinstance(x, Base)

# TypeGuard[Base]がTrueの場合xはBase型として扱われる
def use_typeguard(x: Child | Unrelated) -> None:
    if is_base_typeguard(x):
        reveal_type(x)  # Base
    else:
        reveal_type(x)  # Child | Unrelated

# TypeIs[Base]がTrueの場合xは(Child | Unrelated)型とBase型との交差をとる
def use_typeis(x: Child | Unrelated) -> None:
    if is_base_typeis(x):
        reveal_type(x)  # Child
    else:
        reveal_type(x)  # Unrelated
```

なお`def guard(x: TypeA) -> TypeIs[TypeB]`における`TypeB`は`TypeA`のサブタイプである必要があるため、例えば

```py
def is_str_list(val: list[object]) -> TypeIs[list[str]]:  # Error
    return all(isinstance(x, str) for x in val)
```

このように書くとエラーが出ます。この場合は従来通り`TypeGuard`を使用しましょう。

## おわりに

というわけで3.13までのまとめでした。個人的にはジェネリクスの書き方が変わったのがかなりいいなと思っているので積極的に広めていきたいです。逆に`TypedDict`周りはちょっとやりすぎな気がしますね…。

それではまた3.16でお会いしましょう。

[^cls]: 自明すぎるので