Python Literal Type: Restricting Values with typing.Literal
python literal type: Learn how Python's Literal type restricts function arguments to exact values, how type checkers enforce it, and where runtime validation still mat...
python literal type requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
What the Literal Type Solves
A function parameter annotated as str accepts any string, and an int parameter accepts any integer. Many functions, however, only make sense for a small set of exact values. The Literal type from the typing module lets you declare that a parameter must be one of those specific values, and type checkers will reject anything else.
from typing import Literal def set_log_level(level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]) -> None: print(f"Log level set to {level}")
Calling set_log_level("TRACE") is a static type error because "TRACE" is not part of the declared literal set. This moves validation from runtime checks into the type system, catching invalid arguments before the code runs.
Basic Syntax and Supported Value Types
Literal accepts strings, integers, booleans, bytes, and enum members. Floats and arbitrary expressions are not supported. The values must be hashable and comparable.
from typing import Literal Mode = Literal["read", "write", "append"] Port = Literal[8080, 8443] Flag = Literal[True]
You can assign a Literal alias to a variable and reuse it across multiple function signatures. This keeps the allowed set in one place.
LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR"] def configure_logger(level: LogLevel) -> None: ... def set_global_level(level: LogLevel) -> None: ...
Duplicate values in a Literal are collapsed by the type checker. Literal["a", "a"] is equivalent to Literal["a"].
How Type Checkers Enforce Literal
Mypy, Pyright, and other static type checkers enforce Literal during analysis. The key rule is that a value typed as a broader type cannot be passed where a Literal is expected, even if the actual runtime value happens to match.
def choose(option: Literal["left", "right"]) -> None: ... user_input: str = "left" choose(user_input) # error: Argument 1 has incompatible type "str"
The type checker cannot prove that user_input is restricted to "left" or "right", so it rejects the call. To satisfy the checker, you must narrow the value first:
if user_input in ("left", "right"): choose(user_input) # OK: narrowed to Literal["left", "right"]
This narrowing behavior is important in real codebases. Functions that read from configuration files, environment variables, or user input produce broad types like str, and you need an explicit narrowing step before passing those values to a Literal-typed parameter.
Combining Literal with Union and Optional
Literal composes with Union and Optional. This is useful when a function accepts either a specific set of values or an unconstrained fallback.
from typing import Literal, Optional, Union def parse(data: str, format: Literal["json", "yaml", "toml"] | None = None) -> dict: if format is None: format = "json" ...
The | None makes the parameter optional. The function can be called as parse(data) or parse(data, "yaml"), but not parse(data, "xml").
You can also mix Literal with broader types in a Union:
def open_resource(path: str, mode: Literal["r", "rb", "w", "wb"] | str = "r") -> ...
Here the parameter accepts any string, but the Literal portion documents the common values. Type checkers treat the union correctly, and callers passing "r" or "w" get the narrower inferred type.
Runtime Behavior: Literal Is Not a Runtime Constraint
Literal is erased at runtime. It performs no validation, raises no errors, and adds no performance cost. The annotation exists only for static analysis. If you need runtime enforcement, you must write it yourself.
from typing import Literal, get_args def set_mode(mode: Literal["fast", "safe"]) -> None: allowed = get_args(Literal["fast", "safe"]) if mode not in allowed: raise ValueError(f"Invalid mode: {mode}")
get_args() returns the tuple ("fast", "safe") at runtime, which you can use for explicit validation. This pattern is useful when the same allowed set must be enforced both statically and at runtime, such as in a library that accepts untrusted input.
The runtime cost of get_args() is negligible because it only extracts the values from the annotation object. It does not scan or validate anything else.
Using Literal for Return Types and Overloads
Literal return types are useful for factory functions and discriminated unions. A function that returns a Literal tells callers exactly which value to expect.
from typing import Literal, overload @overload def load(source: Literal["file"]) -> bytes: ... @overload def load(source: Literal["network"]) -> str: ... def load(source: Literal["file", "network"]): if source == "file": return b"file contents" return "network response"
With overloads, the return type depends on the literal argument. Calling load("file") gives bytes; calling load("network") gives str. This is a form of dependent typing that makes the function's contract explicit.
Common Mistakes and Edge Cases
Literal[1.5] is not allowed. The type checker rejects float values because Literal only supports types with a finite set of representable values.
Literal[None] is redundant. Use None directly in a Union instead of Literal[None]. The two are equivalent, but None is clearer.
Boolean literals have a subtle interaction with bool. Literal[True] is a subtype of bool, but Literal[True, False] is equivalent to bool in most type checkers. If you need to distinguish between True and False as distinct literal values, use them separately rather than relying on bool.
Literal values must be hashable. Lists, dictionaries, and sets cannot appear inside Literal. Only strings, integers, booleans, bytes, and enum members are valid.
Maintainability and Production Considerations
Literal types make APIs self-documenting. A reader sees exactly which values a function accepts without reading the implementation. When the allowed set changes, type checkers flag every call site that passes a removed value, which makes refactoring safer.
The tradeoff is coupling. If a Literal set grows large or changes frequently, every call site that depends on the exact values becomes part of the change surface. For a small, stable set like Literal["debug", "info", "error"], Literal is the right choice. For a set that grows beyond a handful of values or needs associated metadata, an Enum is often more maintainable.
from enum import Enum class LogLevel(Enum): DEBUG = "debug" INFO = "info" ERROR = "error" def configure_logger(level: LogLevel) -> None: ...
An Enum gives you a single definition, runtime validation, and the ability to attach attributes to each member. Literal gives you static checking with less ceremony. The decision depends on whether the values need runtime behavior beyond simple equality.
When Literal is used across module boundaries, keep the alias in a shared types module. This avoids duplicating the literal set in every signature and makes the intent visible in one place.