Literal, Final and enums

typing · Practical · 9 min

Mode strings like 'r' or 'json' are bug magnets; Literal catches typos at check time.

Literal["r", "w", "a"] is a type whose only values are those exact constants. Passing "rw" is a checker error, and editors autocomplete the options. Literals can be strings, ints, bools, bytes, enum members or None.

Mode = Literal["fast", "safe"]
def run(mode: Mode = "safe") -> None: ...

Narrowing works on literals: after if mode == "fast":, the checker knows mode is Literal["safe"] in the else branch. Plain str variables aren't accepted where a Literal is expected unless the checker can prove the value, so use Final or literal-typed variables for constants you pass around.

Final marks a name that must not be reassigned (MAX_RETRIES: Final = 3); the checker infers the literal type Literal[3]. On class attributes it also forbids overriding in subclasses. The @final decorator forbids subclassing a class or overriding a method.

Enums are often the better choice when values have behaviour, are used across many modules, or need iteration (for c in Color). Literals shine for small option sets at API boundaries, where users pass plain strings. Both support exhaustiveness checking, covered in the narrowing lesson.

Example

from typing import Final, Literal, get_args

Format = Literal["json", "csv"]
DEFAULT_FORMAT: Final = "json"           # checker: Literal['json'], no reassignment

def export(rows: list[dict[str, int]], fmt: Format = DEFAULT_FORMAT) -> str:
    if fmt == "json":
        return "json:" + str(rows)
    return "csv:" + ",".join(str(r) for r in rows)   # fmt: Literal['csv']

print(export([{"a": 1}]))                # json:[{'a': 1}]
print(export([{"a": 1}], "csv"))         # csv:{'a': 1}
print(get_args(Format))                  # ('json', 'csv')

user_choice = "xml"
if user_choice in get_args(Format):      # validate untrusted strings at runtime
    print("ok")
else:
    print("unsupported:", user_choice)   # unsupported: xml
# export([], "xml")   -> checker error: incompatible type "Literal['xml']"
# DEFAULT_FORMAT = "csv"  -> checker error: cannot assign to final name

Remember

  • Literal[...] limits a value to exact constants
  • Final forbids reassignment and keeps the literal type
  • get_args() turns a Literal into a tuple for runtime validation

Watch out for

  • Assuming Literal validates at runtime; untrusted input still needs an explicit check.

Check yourself

  1. What does get_args(Literal['a', 'b']) return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/typing.html#typing.Literal