Python Literal: Restrict Exact Values

Published on: August 28, 2026
Reading time: 4 minutes
Close-up of a metal gate featuring a 'Please Keep Gate Closed' sign, outdoors.

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 error

At 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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    ZIP file compression icon
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    ZipInfo.compress_level: Control ZIP Compression

    Learn Python ZipInfo.compress_level to choose compression per ZIP member and balance size, CPU, latency, and compatibility.

    Ler mais

    Tempo de leitura: 5 minutos
    24/10/2026
    Binary code representing Python bytearray.resize
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    bytearray.resize: Resize Buffers Safely

    Learn Python bytearray.resize for safe buffer growth and shrinking, limits, memoryview handling, and compatibility.

    Ler mais

    Tempo de leitura: 5 minutos
    24/10/2026
    Python code on a laptop for filtering None values
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    operator.is_none: Filter None Without Losing Falsy

    Learn operator.is_none and is_not_none to filter None values without removing valid falsy data in Python.

    Ler mais

    Tempo de leitura: 4 minutos
    23/10/2026
    Python code on a laptop for creating dynamic dataclasses
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    make_dataclass decorator: Transform Dataclasses

    Learn how the make_dataclass decorator parameter validates, registers, and transforms dynamic dataclasses safely in Python.

    Ler mais

    Tempo de leitura: 5 minutos
    23/10/2026
    Code and XML structure for canonicalization with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    ElementTree canonicalize: Normalize XML in Python

    Learn Python ElementTree canonicalize for stable XML comparisons, hashes, namespaces, comments, and signature preparation.

    Ler mais

    Tempo de leitura: 5 minutos
    22/10/2026
    Python programming code displayed on a screen
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    T-strings and templatelib: Structured Interpolation

    Learn Python t-strings and string.templatelib for structured interpolation, safer renderers, logging, and custom processing.

    Ler mais

    Tempo de leitura: 5 minutos
    22/10/2026