Literalって何?一言でいうと「値そのものを型にする」
Literal は、型ヒントの世界で
「この引数(または値)は、この“決まった値”しか受け付けません」
と表現するための道具です。
普通の型ヒントは「int」「str」など“型”を指定しますが、Literal は「1」「”DEBUG”」「”INFO”」のように“具体的な値”を指定します。
つまり、
「int のどれでもいい」ではなく
「1 か 2 だけ」「’DEBUG’ か ‘INFO’ か ‘ERROR’ だけ」
という制約を、型として書けるようになります。
ここが、Union や Optional と比べたときの一番の特徴です。
まずは超シンプルな例でLiteralの感覚をつかむ
ログレベルを表すLiteralの例
例えば、ログレベルを文字列で指定する関数を考えます。
def set_log_level(level: str) -> None:
...
Pythonこう書いてあると、「どんな文字列でも渡せる」ように見えます。
でも、本当は "DEBUG", "INFO", "ERROR" だけを許したいとします。
そこで Literal の出番です。
from typing import Literal
LogLevel = Literal["DEBUG", "INFO", "ERROR"]
def set_log_level(level: LogLevel) -> None:
...
Pythonこうすると、「この関数に渡してよいのは "DEBUG", "INFO", "ERROR" のどれかだけ」ということが、型としてはっきりします。
Python 3.11 以降なら、typing ではなく typing からそのまま Literal を使えますが、使い方は同じです。
何が嬉しいのか(エディタと静的解析が賢くなる)
例えば、こう書いたとします。
set_log_level("DEBUG") # OK
set_log_level("INFO") # OK
set_log_level("WARN") # 本当はダメにしたい
Pythonlevel: str だと、どれも「型的には OK」です。
でも Literal["DEBUG", "INFO", "ERROR"] にしておくと、
エディタや静的解析ツールが「WARN は許されていない値だよ」と教えてくれます。
実行前に「ありえない値」を弾けるようになる、というのが Literal の強さです。
LiteralとUnionの違いをちゃんと整理する
Unionは「型のどれか」、Literalは「値のどれか」
Union はこうでした。
from typing import Union
value: Union[int, str] # int か str のどちらか
Pythonこれは「型レベルの“どれか”」です。
一方 Literal はこうです。
from typing import Literal
value: Literal[1, 2, 3] # 1 か 2 か 3 のどれか
Pythonこれは「値レベルの“どれか”」です。
もう少し混ぜて書くと、こういうこともできます。
value: Literal[0, 1] # 0 か 1 だけ
mode: Literal["r", "w", "a"] # ファイルモード
PythonUnion は「型のバリエーション」、
Literal は「値のバリエーション」を表現するものだ、と覚えておくと整理しやすいです。
実践的な例1:モードやフラグをLiteralで縛る
ファイルモードをLiteralで表現する
Python の open 関数の mode には、 "r", "w", "a" など、決まった文字列しか渡しません。
自分でラッパー関数を書くときに、こう書けます。
from typing import Literal
FileMode = Literal["r", "w", "a"]
def open_file(path: str, mode: FileMode) -> str:
return f"open {path} with {mode}"
Pythonこの関数に対して、
open_file("data.txt", "r") # OK
open_file("data.txt", "x") # 型的には NG(Literal に含まれていない)
Pythonという区別がつきます。
実行時にエラーになる前に、
「そのモードはサポートしていないよ」とツールが教えてくれるわけです。
フラグ的な引数をLiteralで表す
例えば、「並び順」を指定する関数を考えます。
def sort_items(order: str) -> None:
...
Python本当は "asc" か "desc" だけを許したいなら、こう書けます。
from typing import Literal
Order = Literal["asc", "desc"]
def sort_items(order: Order) -> None:
...
Pythonこれで、 "ascending" みたいな微妙なスペルミスも、
静的解析で検出できるようになります。
実践的な例2:戻り値にLiteralを使って「状態」を表す
処理結果をLiteralで返す
例えば、「処理が成功したかどうか」を文字列で返す関数を考えます。
def process() -> str:
...
Pythonこれだと、「何が返ってくるのか」が曖昧です。
本当は "ok" か "error" のどちらかだけを返す設計なら、
Literal を使ってこう書けます。
from typing import Literal
Result = Literal["ok", "error"]
def process() -> Result:
...
Pythonこれで、「この関数は "ok" か "error" しか返さない」という仕様が、型として固定されます。
呼び出し側も、こういうコードを書きやすくなります。
result = process()
if result == "ok":
...
elif result == "error":
...
Pythonもし "unknown" みたいな値を返すように実装を変えたら、
静的解析ツールが「型と合っていない」と教えてくれます。
Literalがテストと品質にどう効いてくるか
テストケースの「列挙」がしやすくなる
Literal を使うと、「取りうる値」が型として列挙されます。
例えば、さっきの LogLevel を思い出してください。
LogLevel = Literal["DEBUG", "INFO", "ERROR"]
def set_log_level(level: LogLevel) -> None:
...
Pythonこの型を見た時点で、テストの観点がはっきりします。
“DEBUG” のとき
“INFO” のとき
“ERROR” のとき
少なくともこの 3 パターンはテストしたくなりますよね。
型に Literal がないと、「たまたま ‘DEBUG’ だけテストして終わり」になりがちです。
Literal は、「仕様上の全パターン」を型として書き出すことで、
テストケースの漏れを減らしてくれます。
「ありえない値」を早期に潰せる
Literal を使っていると、
「仕様上ありえない値」がコードに紛れ込んだときに、
静的解析で検出できます。
例えば、誰かがこう書いたとします。
set_log_level("debug") # 小文字
PythonPython 的には普通に動きますが、
仕様としては「大文字だけ」を許したいかもしれません。
Literal で "DEBUG" だけを許しておけば、
この時点で「それは許されていない値だよ」と教えてもらえます。
これは、バグを「実行前」に潰すための強力な仕組みです。
Literalを使うときの注意点と「やりすぎ防止」
なんでもLiteralにすると窮屈になる
Literal は強力ですが、
何でもかんでも Literal で縛ると、コードが窮屈になります。
例えば、こういうのはやりすぎです。
from typing import Literal
Age = Literal[18, 19, 20, 21, 22, 23, 24, 25]
Python年齢を「18〜25 のどれか」に限定したい気持ちは分かりますが、
これは型でやる話ではありません。
こういうのは、普通に int にしておいて、
バリデーションでチェックする方が自然です。
Literal が向いているのは、
モード
フラグ
状態
コマンド名
のように、「取りうる値が少なく、列挙できるもの」です。
仕様が頻繁に変わるところには向かない
Literal は、「値の集合」を型として固定します。
ということは、
仕様が変わるたびに型も変えないといけません。
例えば、ログレベルに "TRACE" を追加したくなったら、
LogLevel = Literal["DEBUG", "INFO", "ERROR", "TRACE"]
Pythonと書き換える必要があります。
これは悪いことではありませんが、
「毎週のように増減する値」を Literal で縛るのはしんどくなります。
仕様が安定しているところ、
あるいは「変わるときはちゃんと型も直したいところ」に使うのが向いています。
初心者がLiteralとどう付き合うといいか
まずは「文字列のモード・フラグ」から使ってみる
最初の一歩としては、
ログレベル
並び順(”asc” / “desc”)
ファイルモード(”r” / “w” / “a”)
ステータス(”ok” / “error”)
のような「少数の文字列の選択肢」を Literal で表現してみるのがオススメです。
そこから、
この値のバリエーションは型で縛るべきか?
それとも、ただの文字列+バリデーションで十分か?
という感覚を少しずつ育てていくと、
Literal の「ちょうどいい使いどころ」が見えてきます。
「仕様を列挙するための道具」として意識する
Literal を付けるということは、
「この引数(または戻り値)は、この値たちのどれかに“必ず”なる」
と仕様レベルで宣言することです。
それはそのまま、
テストで全部の値を試す
呼び出し側で間違った値を渡さない
仕様変更時に型も一緒に見直す
という行動につながります。
Literal は、「仕様を列挙して、コードとテストに落とし込むための道具」だと意識して使うと、
設計と品質の両方が一段上がります。
まとめ(Literalは「値のバリエーションを型で固定する」ための道具)
Literal を初心者目線で整理すると、こうなります。
Literal は、「この値はこの“決まった値のどれか”だけを取る」ということを型で表現する仕組みで、Literal["DEBUG", "INFO", "ERROR"] のように具体的な値を列挙できる。
Union が「型のどれか」を表すのに対して、Literal は「値のどれか」を表し、モード・フラグ・状態など、取りうる値が少ないものを縛るのに向いている。
Literal を使うと、「仕様上の全パターン」が型として見えるようになり、テストケースの漏れを減らしつつ、ありえない値を静的解析で早期に検出できる。
ただし、何でも Literal で縛ると窮屈になるので、仕様が安定していて、値のバリエーションを列挙したい場面(ログレベル、ステータス、モードなど)に絞って使うのが現実的で、設計・品質のバランスも取りやすい。
