Algunas funciones nunca regresan normalmente: siempre lanzan una excepción, terminan el proceso o permanecen en un flujo que no finaliza. Otras ramas deberían ser imposibles después de tratar todas las variantes de una unión. typing.Never representa el tipo vacío, sin valores posibles, y permite que los analizadores comprendan esos escenarios.
Esta guía explica funciones que no retornan, assert_never() para exhaustividad, Literal, Enum, match/case, callbacks, la relación con NoReturn y los errores más frecuentes.
El tipo sin valores
Los tipos habituales describen conjuntos de valores. int contiene enteros, str contiene cadenas y int | str contiene ambos. Never describe un conjunto vacío: no existe un valor Python normal que habite ese tipo.
Por ello también se conoce como tipo inferior o bottom type. Puede considerarse subtipo de todos los tipos porque el conjunto vacío está contenido en cualquier conjunto.
Función que siempre lanza
from typing import Never
def fallar(mensaje: str) -> Never:
raise RuntimeError(mensaje)La anotación indica que el flujo nunca continúa después de la llamada.
usuario = buscar_usuario()
if usuario is None:
fallar("usuario no encontrado")
print(usuario.nombre)Después de esa rama, el analizador sabe que usuario ya no puede ser None.
Terminar el proceso
import sys
from typing import Never
def salir_con_error(codigo: int, mensaje: str) -> Never:
print(mensaje, file=sys.stderr)
raise SystemExit(codigo)SystemExit es una excepción, por lo que no existe retorno normal. Un helper que siempre llama a otra función Never también cumple el contrato.
Never frente a None
None es un valor real. Una función anotada con -> None retorna normalmente aunque no produzca un resultado útil.
def registrar(texto: str) -> None:
print(texto)Una función -> Never no alcanza un retorno normal. Confundir ambos contratos es un error.
Never frente a NoReturn
typing.NoReturn apareció específicamente para funciones que nunca regresan. Never generaliza el concepto de tipo vacío y puede aparecer en otros contextos. Para retornos de función, ambos expresan esencialmente el mismo comportamiento.
from typing import NoReturn
def abortar() -> NoReturn:
raise SystemExit(1)El código nuevo puede preferir Never, mientras bibliotecas compatibles con versiones antiguas pueden mantener NoReturn o importar Never desde typing_extensions.
Exhaustividad con assert_never
from typing import Literal, assert_never
Estado = Literal["nuevo", "pagado", "enviado"]
def etiqueta(estado: Estado) -> str:
match estado:
case "nuevo":
return "Nuevo"
case "pagado":
return "Pagado"
case "enviado":
return "Enviado"
case _:
assert_never(estado)Si todos los valores de Estado fueron tratados, el analizador ve estado como Never en el caso por defecto. Si se añade un nuevo Literal sin rama correspondiente, assert_never() genera un error de tipado.
Por qué no usar solo assert False
case _:
assert False, "estado imposible"Esto falla en runtime, pero puede no exigir al analizador que demuestre que el caso es inalcanzable. assert_never() combina verificación estática y defensa en runtime.
Exhaustividad con if/elif
def color(estado: Estado) -> str:
if estado == "nuevo":
return "gris"
elif estado == "pagado":
return "azul"
elif estado == "enviado":
return "verde"
else:
assert_never(estado)El patrón no necesita match/case. Lo importante es que el analizador reduzca progresivamente la unión.
Never con Enum
from enum import Enum, auto
from typing import assert_never
class Rol(Enum):
ADMIN = auto()
EDITOR = auto()
LECTOR = auto()
def permisos(rol: Rol) -> set[str]:
match rol:
case Rol.ADMIN:
return {"leer", "editar", "eliminar"}
case Rol.EDITOR:
return {"leer", "editar"}
case Rol.LECTOR:
return {"leer"}
case _:
assert_never(rol)Añadir un miembro nuevo puede hacer que la función falle en el análisis hasta tratar esa variante.
Uniones de clases
from dataclasses import dataclass
@dataclass
class Texto:
valor: str
@dataclass
class Numero:
valor: float
Nodo = Texto | Numero
def renderizar(nodo: Nodo) -> str:
if isinstance(nodo, Texto):
return nodo.valor
if isinstance(nodo, Numero):
return str(nodo.valor)
assert_never(nodo)La última línea debe seguir siendo imposible mientras Nodo contenga solo esas clases.
Never como parámetro
Un parámetro Never declara que la función no puede llamarse con un valor normal.
def imposible(valor: Never) -> str:
return "no debería ejecutarse"Este patrón aparece en helpers de exhaustividad y APIs genéricas avanzadas, pero es raro en código de aplicación.
Never en inferencia genérica
La inferencia puede producir Never cuando no existe alternativa posible o un valor está vacío sin información útil. El comportamiento exacto varía entre analizadores. En APIs públicas, añade anotaciones explícitas si la inferencia de vacíos puede confundir.
Callbacks que nunca regresan
from collections.abc import Callable
def ejecutar_o_abortar(
operacion: Callable[[], int],
abortar: Callable[[Exception], Never],
) -> int:
try:
return operacion()
except Exception as error:
abortar(error)Como el callback de aborto no retorna, la función externa no necesita otro return en la rama de excepción.
Bucles infinitos
def servidor() -> Never:
while True:
atender_siguiente_peticion()Una función con bucle demostrablemente infinito puede anotarse como Never. Si existe break, retorno condicional u otro camino de salida, la anotación puede ser falsa.
Los generadores no son funciones Never
Un generador puede producir valores indefinidamente, pero llamar a la función devuelve inmediatamente un objeto generador.
from collections.abc import Iterator
def contar() -> Iterator[int]:
numero = 0
while True:
yield numero
numero += 1El iterador es infinito, pero su creación retorna normalmente.
Función que a veces falla
def cargar(ruta: str) -> bytes:
if not existe(ruta):
raise FileNotFoundError(ruta)
return leer(ruta)La función devuelve bytes en algunos caminos, por lo que su retorno es bytes, no bytes | Never. Never no añade valores a una unión.
Never con overloads
from typing import Literal, overload
@overload
def convertir(valor: str, *, estricto: Literal[True]) -> int: ...
@overload
def convertir(valor: str, *, estricto: Literal[False]) -> int | None: ...
def convertir(valor: str, *, estricto: bool) -> int | None:
try:
return int(valor)
except ValueError:
if estricto:
fallar("entero inválido")
return NoneEl helper Never permite entender que la rama estricta no continúa con None.
Exhaustividad y evolución de APIs
El mayor beneficio de assert_never() aparece cuando los tipos evolucionan. Añadir una variante a una unión, Enum o Literal hace fallar las funciones exhaustivas en CI hasta tratar el nuevo caso. Esto evita defaults silenciosos que ocultan reglas de negocio ausentes.
Cuándo conviene un fallback
Los datos externos pueden contener valores desconocidos aunque la anotación diga lo contrario. En una frontera no confiable, un error de validación, log o fallback puede ser mejor que assert_never. Usa exhaustividad después de validar y controlar los valores.
Runtime de assert_never
Si se llama, assert_never() lanza una excepción porque recibió un valor supuestamente imposible. Es una defensa útil, pero su finalidad principal es que el analizador compruebe el argumento.
Compatibilidad de versiones
Never y assert_never están disponibles en versiones modernas de typing. Para intérpretes anteriores, usa typing_extensions.Never y typing_extensions.assert_never.
Errores comunes
- Anotar como Never una función que devuelve None: son contratos diferentes.
- Usar Never en un generador infinito: la llamada devuelve un iterador.
- Ocultar un fallback real: los datos externos pueden tener variantes nuevas.
- Llamar assert_never antes de refinar por completo: aún quedan casos válidos.
- Implementar una función que puede retornar: la anotación queda falsa.
- Confundir NoReturn con no producir valor útil: significa no regresar normalmente.
Ejemplo completo: comandos exhaustivos
from dataclasses import dataclass
from typing import assert_never
@dataclass
class Crear:
nombre: str
@dataclass
class Renombrar:
id: int
nombre: str
@dataclass
class Eliminar:
id: int
Comando = Crear | Renombrar | Eliminar
def ejecutar(comando: Comando) -> None:
match comando:
case Crear(nombre=nombre):
crear(nombre)
case Renombrar(id=id_, nombre=nombre):
renombrar(id_, nombre)
case Eliminar(id=id_):
eliminar(id_)
case _:
assert_never(comando)Añadir una nueva clase a Comando obliga a actualizar el dispatcher. El fallo aparece durante el análisis, antes de producción.
Buenas prácticas
Usa Never en helpers pequeños que realmente terminan el flujo. Usa assert_never después de un refinamiento completo, no como sustituto de validación. Ejecuta un analizador en CI y trata los fallos de exhaustividad como cambios obligatorios de reglas.
Conclusión
typing.Never representa la ausencia total de valores y describe funciones que no regresan, callbacks abortivos y caminos inalcanzables. Con assert_never(), uniones y Enums se convierten en contratos exhaustivos.
La documentación oficial de Never en Python y assert_never define el comportamiento. Usa Never solo cuando no existe retorno normal y reserva fallbacks para datos abiertos o no validados.







