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

    Close-up view of a computer screen displaying code in a software development environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python MappingProxyType: Read-Only Dict

    Learn Python MappingProxyType to expose read-only dictionaries, create dynamic views and snapshots, preserve invariants, and avoid unnecessary copies.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python TypedDict: Typed Dictionaries

    Learn Python TypedDict for typed dictionaries, optional keys, NotRequired, Required, API payloads, discriminated variants, and static safety.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Protocol: Structural Typing

    Learn Python Protocol for structural typing, generic contracts, callbacks, runtime_checkable, testing, dependency injection, and low coupling.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python memoryview: Zero-Copy Buffers

    Learn Python memoryview for zero-copy buffers, slices, writable bytearray data, cast, mmap, struct, sockets, and safe lifecycle management.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python tracemalloc: Track Memory

    Learn Python tracemalloc to measure peaks, create and compare snapshots, filter allocations, and diagnose memory growth and leaks.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Commuters line up at a subway station platform, showcasing public transportation dynamics.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python queue: Communicate Between Threads

    Learn Python queue for thread communication with FIFO, LIFO, priority, backpressure, task tracking, sentinels, and safe shutdown.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026