Una anotación como str indica que una función acepta texto, pero no especifica qué textos son válidos. Muchos parámetros admiten un conjunto cerrado como "json", "csv" o "xml". typing.Literal permite expresar esos valores exactos en la firma, mejorando autocompletado, documentación y análisis estático.
Esta guía explica Literal con strings, números, booleanos, overloads, discriminadores de TypedDict, match/case, aliases reutilizables, comprobación exhaustiva y validación en runtime.
Qué significa Literal
Literal describe un valor específico, no solo su tipo general.
from typing import Literal
Formato = Literal["json", "csv", "xml"]
def exportar(datos: list[dict], formato: Formato) -> bytes:
...
exportar([], "json")
# exportar([], "yaml") # error del analizadorDurante la ejecución, la anotación no bloquea valores inválidos. Las entradas externas siguen necesitando validación.
Cuándo mejora una API
Literal funciona bien cuando el conjunto de opciones es pequeño, estable y forma parte del contrato público. Sustituye strings mágicos por elecciones visibles para el editor.
ModoApertura = Literal["lectura", "escritura", "anexar"]
def abrir_recurso(ruta: str, modo: ModoApertura) -> None:
...El editor puede sugerir todos los valores permitidos, algo más útil que un parámetro str acompañado solo por un comentario.
Strings, números y booleanos
Literal admite valores exactos compatibles con el sistema de typing.
Nivel = Literal[0, 1, 2, 3]
Direccion = Literal["norte", "sur", "este", "oeste"]
Activo = Literal[True]
def configurar(nivel: Nivel, direccion: Direccion, activo: Activo) -> None:
...Literal[True] es más específico que bool. Puede ser útil en overloads, aunque muchos flags booleanos quedan más claros con funciones separadas o enums.
Aliases reutilizables
Crea un alias cuando el mismo conjunto aparezca en varias firmas.
from typing import Literal, TypeAlias
NivelLog: TypeAlias = Literal["debug", "info", "warning", "error"]
def log(mensaje: str, nivel: NivelLog = "info") -> None:
...Los aliases evitan duplicación y ofrecen un único lugar para evolucionar el contrato.
Literal con overload
Uno de los usos más potentes es relacionar un valor exacto con el tipo retornado.
from typing import Literal, overload
@overload
def cargar(ruta: str, *, binario: Literal[True]) -> bytes:
...
@overload
def cargar(ruta: str, *, binario: Literal[False] = False) -> str:
...
def cargar(ruta: str, *, binario: bool = False) -> str | bytes:
modo = "rb" if binario else "r"
with open(ruta, modo) as archivo:
return archivo.read()Al llamar cargar("datos.bin", binario=True), el analizador sabe que el resultado es bytes. Sin Literal, todos los consumidores deberían manejar str | bytes.
Variables e inferencia ampliada
Un literal escrito directamente puede conservar un tipo específico, mientras una variable puede ampliarse a str.
formato = "json"
exportar([], formato) # puede inferirse como str
formato_exacto: Formato = "json"
exportar([], formato_exacto)Usa una anotación explícita cuando necesites preservar el tipo literal. Final también puede ayudar con constantes.
from typing import Final
FORMATO_PREDETERMINADO: Final = "json"Discriminar variantes de TypedDict
Literal combina muy bien con TypedDict. Una clave discriminadora permite identificar la variante correcta.
from typing import Literal, TypedDict
class EventoCreado(TypedDict):
tipo: Literal["creado"]
id: int
class EventoError(TypedDict):
tipo: Literal["error"]
mensaje: str
Evento = EventoCreado | EventoError
def procesar(evento: Evento) -> str:
if evento["tipo"] == "creado":
return f"ID {evento['id']}"
return evento["mensaje"]Dentro de cada rama, la unión se reduce automáticamente a la forma correspondiente.
Literal y match/case
El pattern matching es más fácil de analizar cuando la entrada tiene un tipo literal cerrado.
Comando = Literal["iniciar", "detener", "estado"]
def ejecutar(comando: Comando) -> str:
match comando:
case "iniciar":
return "iniciado"
case "detener":
return "detenido"
case "estado":
return "activo"Las herramientas estáticas pueden detectar casos imposibles o falta de cobertura.
Exhaustividad con assert_never
assert_never() hace visibles los casos olvidados.
from typing import assert_never
def ejecutar(comando: Comando) -> str:
if comando == "iniciar":
return "iniciado"
if comando == "detener":
return "detenido"
if comando == "estado":
return "activo"
assert_never(comando)Si se añade un comando al alias, el analizador puede indicar que la última rama ahora es alcanzable.
Literal frente a Enum
Literal es ligero para conjuntos pequeños usados principalmente en firmas. Enum puede ser mejor cuando los valores necesitan métodos, nombres, iteración o comportamiento de dominio.
from enum import Enum
class FormatoEnum(str, Enum):
JSON = "json"
CSV = "csv"La guía sobre enums en Python explica la alternativa nominal. La elección debe mantener claras las llamadas y la validación.
Códigos numéricos y sentinelas
Literal puede describir un conjunto pequeño de códigos o valores centinela.
EstadoHTTP = Literal[200, 201, 204, 400, 404, 500]
Sentinela = Literal["AUTO", "DEFAULT"]No copies registros enormes o cambiantes dentro de una anotación. Un enum, objeto de valor o búsqueda dinámica puede ser más sostenible.
LiteralString es diferente
LiteralString representa strings construidas a partir de literales confiables y está orientado a APIs sensibles a inyección. No significa una lista cerrada de textos.
from typing import LiteralString
def ejecutar_sql(consulta: LiteralString) -> None:
...Usa Literal["a", "b"] para opciones exactas y LiteralString para restringir el origen de una cadena.
Validación en runtime
Literal no rechaza valores durante la ejecución.
FORMATOS = {"json", "csv", "xml"}
def exportar_seguro(datos: list[dict], formato: str) -> bytes:
if formato not in FORMATOS:
raise ValueError(f"formato inválido: {formato}")
...Un diseño común valida la cadena externa y luego la entrega a una capa interna tipada. Pydantic y otras bibliotecas también interpretan Literal para validar y generar schemas.
Compatibilidad de APIs públicas
Añadir un valor nuevo puede parecer compatible, pero consumidores con manejo exhaustivo quizá necesiten cambios. Eliminar o renombrar una opción es claramente incompatible.
Documenta la semántica de cada alternativa. La escritura exacta no explica efectos secundarios, rendimiento, orden o errores.
Errores comunes
- Usar Literal para datos dinámicos: funciona mejor con conjuntos pequeños y estables.
- Confiar en runtime: las entradas no confiables siguen necesitando validación.
- Repetir listas largas: crea aliases.
- Usar decenas de valores sin estructura: considera Enum o un objeto de configuración.
- Ignorar inferencia ampliada: anota variables cuando importe la exactitud.
- Aplicar cast a valores inválidos: cast no valida.
Ejemplo completo: salida de informes
from typing import Literal, overload
Salida = Literal["texto", "bytes"]
@overload
def generar_informe(*, salida: Literal["texto"]) -> str:
...
@overload
def generar_informe(*, salida: Literal["bytes"]) -> bytes:
...
def generar_informe(*, salida: Salida) -> str | bytes:
contenido = "resultado"
if salida == "texto":
return contenido
return contenido.encode("utf-8")
texto = generar_informe(salida="texto")
binario = generar_informe(salida="bytes")El editor conoce el resultado exacto de cada llamada y ofrece los métodos apropiados sin casts.
Conclusión
typing.Literal expresa elecciones exactas dentro del sistema de tipos. Mejora contratos, autocompletado, overloads, variantes discriminadas y manejo exhaustivo.
La documentación oficial de Literal en Python detalla los valores aceptados y su equivalencia. Úsalo para conjuntos cerrados y estables, conserva validación en runtime y elige Enum cuando el dominio necesite comportamiento propio.







