skip to content

What does typing.Literal["read", "write"] express that a plain str annotation cannot?

level: middleimportance: must knowfreq 55%

answer

  1. Narrower than str
  2. The type of an exact value
  3. Checker-only, runtime is inert
  4. str, bytes, int, bool, enum, None
  5. get_args recovers the allowed values

basics

~20 s

typing.Literal pins a value down to an exact set of allowed constants, so a checker accepts only "read" or "write" where a str annotation would accept any string. It constrains nothing at runtime; CPython still passes any string through.

solid answer

~40 s

`Literal["read", "write"]` is a type whose only members are those two exact string values. A static checker will reject `open_track(mode="apend")` at the call site, while `mode: str` accepts every typo. Inside the function the checker also knows the value is one of two constants, which is what lets it reason about each branch separately. The runtime is entirely uninvolved: `Literal` is a typing construct, the annotation is not evaluated as a guard, and a bad string still reaches your code from untyped callers or from JSON. So `Literal` is documentation plus a compile-time gate, not validation — if the value crosses a trust boundary you still check it yourself. Only `str`, `bytes`, `int`, `bool`, enum members and `None` may appear inside it; floats, computed expressions and variables are rejected by checkers.

code

python · 9 lines
python
from typing import Literal, get_args

Mode = Literal["read", "write", "append"]

def open_track(name: str, mode: Mode = "read") -> str:
    return f"{name}:{mode}"

print(get_args(Mode))
print(open_track("contigs.tsv", "append"))

go deeper

for a junior

Be ready to say what Literal["read", "write"] allows and that a plain str annotation would accept any string at all. Knowing it is a hint a checker reads, not a runtime check, is enough at this level.

for a middle

Explain the mechanics: literal types are subtypes of their base type, assignment to an unannotated variable widens them back to str, and only str, bytes, int, bool, enum members and None may appear inside.

for a senior

Show where the guarantee stops. Values crossing a deserialisation or CLI boundary arrive as plain strings, so you pair the annotation with one explicit runtime check — often driven by typing.get_args on the same alias.

for a principal

Own the API-surface argument: a named literal alias makes a public signature self-documenting at zero import cost for callers, and you should be able to say when that stops being enough and a richer construct earns its keep.

## The problem `Literal` solves Most Python APIs have parameters whose real domain is far smaller than their annotation says. A function that takes `mode: str` usually accepts exactly three strings; the annotation advertises several billion. `typing.Literal`, added in Python 3.8 by PEP 586, closes that gap: `Literal["read", "write"]` is a type whose only inhabitants are those two exact values. ```python from typing import Literal Mode = Literal["read", "write", "append"] def open_track(name: str, mode: Mode = "read") -> str: ... ``` `open_track("contigs.tsv", "apend")` is now a static error at the call site. With `mode: str` it is a runtime bug that surfaces later, probably as a silent fallback in an `else` branch. ## How a checker reads it Two rules do most of the work. **Subtyping is one-directional.** The type of the expression `"read"` is `Literal["read"]`, which is a subtype of `str`. So a `Literal["read"]` value may be passed where `str` is expected, but a `str` value may not be passed where `Literal["read", "write"]` is expected — that assignment is exactly the mistake you wanted flagged. This is also why reading a mode out of a config dictionary typed `dict[str, str]` and passing it straight in is an error: the checker has lost the literal-ness. **Literal types widen on assignment unless you stop them.** `m = "read"` infers `str` for a mutable variable, because you could rebind it. Annotating (`m: Mode = "read"`) or declaring the name `Final` keeps the narrow type. Inside the function body a checker treats the parameter as a union of two singleton types, so a comparison such as `if mode == "read"` genuinely narrows it. That narrowing behaviour, and the exhaustiveness reasoning built on top of it, is the payoff `Literal` exists for. ## What may appear inside it The typing specification allows only `str`, `bytes`, `int`, `bool`, enum members, `None`, and nested `Literal` types. Floats are excluded (equality on floats is a trap), and so are variables and computed expressions: `Literal[MAX]` is meaningless to a checker even though the interpreter builds the object happily. That last point matters — **the runtime does not police any of this**. `Literal[1.5]` constructs without complaint at the REPL. Only the checker rejects it. The runtime does normalise two things. Duplicate parameters collapse (`Literal["read", "read"]` becomes `Literal["read"]`), and since Python 3.9.1 equality is order-insensitive and type-aware, so `Literal["read", "write"] == Literal["write", "read"]` is `True` while `Literal[True] == Literal[1]` is `False` — the latter despite `True == 1` being true for the values themselves. ## Reuse and introspection Give the union a name — `Mode = Literal["read", "write", "append"]` — and it becomes one edit point instead of a string repeated across a dozen signatures. At runtime `typing.get_args(Mode)` returns the tuple `('read', 'write', 'append')`, which is how libraries generate CLI choices, validators or documentation from the same declaration the checker uses. That trick is the honest way to get runtime validation out of a `Literal`: you write the check, driven by the annotation. ## `LiteralString` Python 3.11 added `typing.LiteralString` (PEP 675), a related but different idea. `Literal["read"]` means one specific value; `LiteralString` means *any* string that a checker can prove was built entirely from string literals in your source — concatenation and `%`/`f-string` composition of literals included, but never a value that came from a request, a file or a database. It is the tool for marking a parameter that must never receive externally supplied text. ## What it is not `Literal` is not validation. Nothing at runtime rejects a bad value; unchecked code, deserialised JSON and dynamic call sites all bypass it. It is also not a substitute for a named set of constants when those constants need behaviour or metadata attached — that is what an enum is for, and the two interoperate, since enum *members* are legal inside `Literal`. Used well, `Literal` is one of the cheapest wins in typed Python: no import for your callers, no runtime cost, a signature that documents its own domain, and a checker that catches the typo before it ships. ## Where checkers surprise people Three behaviours account for most of the confusion in real code. First, a literal type assigned to a plain variable widens: after `m = "read"`, the inferred type of `m` is `str`, so passing it to a `Literal` parameter fails until you annotate the variable or declare it `Final`. Second, a value pulled out of a container typed `dict[str, str]` is a `str` and nothing more — the checker has no way to know the config file only ever holds legal modes, and it is right not to guess. Third, `Literal` composes with `None` and with other types in a union, so `Literal["read", "write"] | None` is the ordinary way to spell an optional mode, and an enum *member* is a legal parameter, which lets a signature accept one specific member of an existing enum without accepting the whole class. Each of those is the checker doing its job: the type of an exact value is a genuinely narrower thing than the type of a variable that merely happens to hold it right now.

  • Why can a Literal["read"] value be passed to a parameter annotated str, but not the reverse?
    Because `Literal["read"]` is a subtype of `str` — every value of that type is a string, so it satisfies the wider annotation. The reverse fails: an arbitrary `str` is not provably one of the listed constants, so a checker rejects passing it where `Literal["read", "write"]` is required. This is ordinary subtyping, and it is why a value read out of a `dict[str, str]` must be narrowed or validated before it can be used as a mode.
  • What is typing.LiteralString and how does it differ from Literal["read"]?
    `Literal["read"]` is one exact value. `LiteralString`, added in 3.11 by PEP 675, is the type of any string a checker can prove was assembled purely from literals written in the source — including concatenation and f-string composition of literals. It marks parameters that must never receive externally supplied text; a value derived from user input is a plain `str` and is rejected there.
  • What can you legally put inside typing.Literal, and what happens if you break that rule?
    Only `str`, `bytes`, `int`, `bool`, enum members, `None`, and nested `Literal` types. Floats, variables and computed expressions are not allowed. Nothing enforces this at runtime — `Literal[1.5]` builds a perfectly ordinary typing object at the REPL — so the error surfaces only when a static checker runs. That asymmetry catches people who expect the interpreter to complain.

saying these in an interview costs you the question

  • Claiming Literal validates the value at runtime
  • Saying Literal["read"] and str are interchangeable in both directions
  • Putting a float or a variable inside Literal
  • Confusing Literal with LiteralString
  • Thinking Literal[True] and Literal[1] are the same type

context