An annotation such as str says that a function accepts text, but it does not say which text values are valid. Many parameters accept a closed set such as "json", "csv", or "xml". typing.Literal expresses those exact values in a signature, improving autocomplete, documentation, and static analysis.
This guide explains Literal with strings, numbers, booleans, overloads, TypedDict discriminators, match/case, reusable aliases, exhaustiveness checks, and runtime validation.
What Literal means
Literal describes a specific value rather than only its general type.
from typing import Literal
Format = Literal["json", "csv", "xml"]
def export(data: list[dict], format: Format) -> bytes:
...
export([], "json")
# export([], "yaml") # static checker errorAt runtime, the annotation does not block invalid values. External input still needs validation.
When Literal improves an API
Literal works best when the option set is small, stable, and part of the public contract. It replaces undocumented magic strings with editor-supported choices.
OpenMode = Literal["read", "write", "append"]
def open_resource(path: str, mode: OpenMode) -> None:
...An editor can suggest every allowed value, which is more useful than a broad str parameter and a comment.
Strings, numbers, and booleans
Literal supports exact values that are valid in the typing system.
Level = Literal[0, 1, 2, 3]
Direction = Literal["north", "south", "east", "west"]
Enabled = Literal[True]
def configure(level: Level, direction: Direction, enabled: Enabled) -> None:
...Literal[True] is more specific than bool. It can be useful in overloads, although boolean flags sometimes indicate that separate functions or an enum would be clearer.
Reusable aliases
Create an alias when the same set appears in several signatures.
from typing import Literal, TypeAlias
LogLevel: TypeAlias = Literal["debug", "info", "warning", "error"]
def log(message: str, level: LogLevel = "info") -> None:
...Aliases avoid duplication and provide one place to evolve the contract.
Literal with overload
One of the strongest uses is linking an exact argument value to a return type.
from typing import Literal, overload
@overload
def load(path: str, *, binary: Literal[True]) -> bytes:
...
@overload
def load(path: str, *, binary: Literal[False] = False) -> str:
...
def load(path: str, *, binary: bool = False) -> str | bytes:
mode = "rb" if binary else "r"
with open(path, mode) as file:
return file.read()When code calls load("data.bin", binary=True), the checker knows the result is bytes. Without Literal, every caller would handle str | bytes.
Variables and widened inference
A directly written literal can remain specific in some contexts, while a variable may be widened to str.
format = "json"
export([], format) # may be inferred as str
exact_format: Format = "json"
export([], exact_format)Use an explicit annotation when the literal type must be preserved. Final can also help with constants.
from typing import Final
DEFAULT_FORMAT: Final = "json"Discriminating TypedDict variants
Literal combines well with TypedDict. A discriminator key lets a checker identify the correct variant.
from typing import Literal, TypedDict
class CreatedEvent(TypedDict):
kind: Literal["created"]
id: int
class ErrorEvent(TypedDict):
kind: Literal["error"]
message: str
Event = CreatedEvent | ErrorEvent
def process(event: Event) -> str:
if event["kind"] == "created":
return f"ID {event['id']}"
return event["message"]Inside each branch, the union narrows to the matching dictionary shape.
Literal and match/case
Pattern matching becomes easier to analyze when the input has a closed literal type.
Command = Literal["start", "stop", "status"]
def execute(command: Command) -> str:
match command:
case "start":
return "started"
case "stop":
return "stopped"
case "status":
return "active"Static tools may detect unreachable cases or missing coverage, especially when combined with an unreachable helper.
Exhaustiveness with assert_never
assert_never() makes missing cases visible.
from typing import assert_never
def execute(command: Command) -> str:
if command == "start":
return "started"
if command == "stop":
return "stopped"
if command == "status":
return "active"
assert_never(command)If a new command is added to the alias, the checker can report that the final branch is now reachable.
Literal versus Enum
Literal is lightweight for small option sets used mainly in signatures. Enum may be better when values need methods, names, iteration, richer documentation, or domain behavior.
from enum import Enum
class FormatEnum(str, Enum):
JSON = "json"
CSV = "csv"The guide to Python enums explains the nominal alternative. Choose the representation that keeps call sites and validation clear.
Numeric codes and sentinels
Literal can describe a small set of numeric codes or sentinel values.
HttpStatus = Literal[200, 201, 204, 400, 404, 500]
Sentinel = Literal["AUTO", "DEFAULT"]Do not duplicate enormous or frequently changing registries inside annotations. An enum, constrained value object, or runtime lookup may be easier to maintain.
LiteralString is different
LiteralString represents strings built from trusted literals and is intended for injection-sensitive APIs. It does not mean a closed list of exact values.
from typing import LiteralString
def execute_sql(query: LiteralString) -> None:
...Use Literal["a", "b"] for exact options and LiteralString for trusted string origins in compatible checkers.
Runtime validation
Literal does not reject values during execution.
FORMATS = {"json", "csv", "xml"}
def safe_export(data: list[dict], format: str) -> bytes:
if format not in FORMATS:
raise ValueError(f"invalid format: {format}")
...A common design validates external strings and then passes a narrowed value to an internal typed layer. Pydantic and similar libraries can also understand Literal and generate validation or schema information.
Public API compatibility
Adding a new allowed literal may look backward compatible, but consumers with exhaustive handling may need updates. Removing or renaming a value is clearly breaking.
Document the semantics of every option. Exact spelling alone does not explain side effects, performance, ordering, or error behavior.
Common mistakes
- Using Literal for dynamic data: it is best for small stable sets.
- Trusting it at runtime: untrusted input still needs validation.
- Repeating long value lists: create aliases.
- Using dozens of values without structure: consider Enum or a configuration object.
- Ignoring widened inference: annotate variables when exactness matters.
- Casting invalid data: a cast does not validate a value.
Complete example: report output
from typing import Literal, overload
Output = Literal["text", "bytes"]
@overload
def build_report(*, output: Literal["text"]) -> str:
...
@overload
def build_report(*, output: Literal["bytes"]) -> bytes:
...
def build_report(*, output: Output) -> str | bytes:
content = "result"
if output == "text":
return content
return content.encode("utf-8")
text = build_report(output="text")
binary = build_report(output="bytes")The editor knows the exact result of each call and offers the appropriate methods without casts.
Conclusion
typing.Literal expresses exact choices in Python’s type system. It improves contracts, autocomplete, overloads, discriminated variants, and exhaustive handling.
The official Python Literal documentation covers accepted values and equivalence. Use Literal for closed stable sets, keep runtime validation for external data, and prefer Enum when the domain needs behavior of its own.







