Never en Python: marca código inalcanzable

Publicado el: 29/08/2026
Tempo de leitura: 5 minutos
Vivid close-up of code on a computer screen showcasing programming details.

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 += 1

El 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 None

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

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Detailed view of programming code in a dark theme on a computer screen.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Concatenate en Python: cambia parámetros

    Aprende Concatenate en Python para añadir u ocultar parámetros iniciales en decoradores tipados con ParamSpec y dependencias.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ParamSpec en Python: conserva firmas

    Aprende ParamSpec en Python para conservar firmas completas en decoradores, callbacks, wrappers async y funciones de orden superior.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Close-up of a python snake curled up, showcasing its detailed scales.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    TypeIs en Python: refina ambas ramas

    Aprende TypeIs en Python para refinar las ramas verdadera y falsa, compararlo con TypeGuard y crear predicados de tipo seguros.

    Ler mais

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

    TypeGuard en Python: refina tipos seguros

    Aprende TypeGuard en Python para refinar tipos y validar colecciones, TypedDict, Protocol y datos externos con comprobaciones reales.

    Ler mais

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

    typing.Self en Python: retornos fluidos

    Aprende typing.Self en Python para métodos fluidos, classmethods, builders, clones, Protocol, context managers y retornos que conservan subclases.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Detailed view of programming code in a dark theme on a computer screen.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    ExceptionGroup en Python: múltiples errores

    Aprende ExceptionGroup en Python para múltiples errores, except*, grupos anidados, TaskGroup, filtros, logging y validación por lotes.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026