PythonのLiteral型について調べてみた（PEP586）

最近ちらほらとVS Code上での各ライブラリの操作でLiteral型関係が目に付いたり他のPEPの資料を読んでいて見かけたりしていたのでLiteral型のPEP資料（PEP586）の内容を軽くメモしておきます。

# どんな型なのか

Pythonビルトインのイミュータブルな値に対して、特定の値のみを受け付けるようにする型です。例えば`Literal[42, 10]`というLiteralの型であれば42か10の整数のみを受け付けるようになりますし、`Literal["apple", "orange"]`という型であればappleもしくはorangeという型のみ受け付けるようになります（値は整数と文字列など複数の型を混ぜることもできます）。

enumなどと少し用途が似ています（enumとの違いは後の節で少し触れます）。

# 使えるPythonバージョン

Python3.8以降のバージョンで使用することができます。3.8以降であればビルトインのtypingパッケージにLiteral型が入っているのでそちらで利用できます。

```py
from typing import Literal


def any_func(value: Literal["apple", "orange"]) -> None:
    ...
```

Python3.7は本記事執筆時点ではまだEoLを迎えていないため、Pythonライブラリなどで3.7をサポートする場合などではtyping_extensionsパッケージを古いPython環境にバックポートする必要があります（mypyなどをインストールしていれば同時にインストールされているとは思います）。古いPythonバージョンで且つ未インストールの場合は事前にインストールしておく必要があります。

```
$ pip install typing-extensions
```

インストールした後はtyping_extensionsパッケージからLiteralクラスがインポートできます。

```py
from typing_extensions import Literal


def any_func(value: Literal["apple", "orange"]) -> None:
    ...
```

# 主な型チェックの挙動

全てではありませんが、部分的にLiteral型による型チェックの挙動を見ていきます。

※本記事での型チェックにはVS Code + Pylance（内部ではPyright）を使っていきます。mypyでも基本的に同じように動作します。その辺のライブラリなどは古い記事ですが過去にいくつか記事にしているので必要な場合それらもご確認ください。

https://qiita.com/simonritchie/items/33ca57cdb5cb2a12ae16

https://qiita.com/simonritchie/items/7492d1c1a3c13b2f27aa

https://qiita.com/simonritchie/items/045fd2373565f3e49036

まずは通常の許容される指定です。以下のように`value`引数に`Literal["apple", "orange"]`と型アノテーションがされている場合、`apple`や`orange`といった文字列であれば型が許容されます。たとえば以下のようなコードでの関数呼び出しでは型でエラーになりません。

```py
from typing import Literal


def any_func(value: Literal["apple", "orange"]) -> None:
    ...


any_func(value="apple")
```

一方で、Literalで設定されていない文字列を指定すると型のエラーになります。

```py
from typing import Literal


def any_func(value: Literal["apple", "orange"]) -> None:
    ...


any_func(value="melon")
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/400cb839-3613-1794-1810-6cc506a49e6d.png)

整数と文字列など、イミュータブルな型であれば複数の型の値をLiteral型に指定することもできます（ただしタプルはPylanceで試していた感じ弾かれたりするようです）。たとえば以下の指定では`apple`という文字列や3という整数、Falseの真偽値などを受け付けてくれます。

```py
from typing import Literal


def any_func(value: Literal["apple", 3, False]) -> None:
    ...


any_func(value=3)
```

enumの値も混ぜることができます。

```py
from typing import Literal
from enum import Enum


class Fruit(Enum):
    APPLE = 1
    ORANGE = 2


def any_func(value: Literal["apple", 3, False, Fruit.APPLE]) -> None:
    ...


any_func(value=3)
any_func(value=Fruit.APPLE)
```

Noneを受け付ける場合にはLiteral側にNoneを含める形でもOptionalで囲む形でもどちらでも動作します。

```py:Literal内にNoneを含めるパターン
from typing import Literal


def any_func(value: Literal["apple", None]) -> None:
    ...


any_func(value=None)
```

```py:Optionalで囲むパターン
from typing import Literal, Optional


def any_func(value: Optional[Literal["apple"]]) -> None:
    ...


any_func(value=None)
```

# enumと比べたメリット

いくつかenumよりも使いやすいケースがあると思いますのでそれらに触れていきます。逆にenumの方が便利なケースも色々あると思いますので適材適所的に選択する形が良さそうです。

## 単純に整数や文字列などが欲しいだけの場合記述がシンプルになる

シンプルに引数などで文字列や整数などが欲しいだけの場合、enumの場合Enumを継承したクラスを定義したり、もしくは値にアクセスする場合にも`value`属性へのアクセスが必要になったり・・・と記述が増えます。外部から使う場合にはそのenumの定義のimportなども必要になったりします。

```py:雑ですがenumを使った例
from typing import Any
from enum import Enum


class Fruit(Enum):
    APPLE = "apple"
    ORANGE = "orange"


def any_func(fruit: Fruit) -> Any:
    if fruit == Fruit.APPLE:
        return ...
    return ...


value = any_func(fruit=Fruit.APPLE)
```

Literal型だとビルトインの文字列や数値などを指定すればOKとなるので記述がシンプルになったり、もしくは対象のenumの定義をimportなどしなくとも済みます。

```py
from typing import Any, Literal


def any_func(fruit: Literal["apple", "orange"]) -> Any:
    if fruit == "apple":
        return ...
    return ...


value = any_func(fruit="apple")
```

## 既存の数値や文字列などを受け付けるインターフェイスの互換性を保ったまま型の恩恵を受ける

型アノテーションの機能が入る以前の昔から存在するビルトインのインターフェイスやライブラリのインターフェイスなどで、引数で特定の数値や文字列などを受け付ける・・・といった場合に既存のインターフェイスをenumに書き換えて互換性が無くなったりとせずにLiteral型で型チェックの恩恵を受けることができます。

例えばwithステートメントで良く使われるopen系のインターフェイスではmodeの文字列などを指定する引数がありますが、そちらは`"r"`や`"w"`、`"a"`、`"rt"`などの特定の文字列だけを受け付けたい（サポートしていない文字列が指定されたら型チェックでエラーとしたい）場合などがあると思います。

そういった場合にLiteral型を使うことで（enumなどで書き換えなくとも）今まで通りに文字列を受け付ける形のままで特定の文字列のみを受け付ける・・・といった制御ができるようになります。アップデート時などにユーザー側に取っては互換性が保たれていて快適ですし、サポートしていない文字列を指定してしまった際にリアルタイムの型チェックなどでもミスに気づきやすくなります。

openなどのインターフェイスの他にも例えばサードパーティのライブラリとしてNumPyやPandasなどでaxis引数に整数を受け付ける状態を維持しつつも0か1を受け付けたいといったケースもあるでしょうし、Pandasのto_dict引数に`"records"`などの特定の文字列を受け付けたい・・・といったようなケースもあると思います。

# 参考文献・参考サイト

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