ExceptionGroup en Python: múltiples errores

Publicado el: 28/08/2026
Tempo de leitura: 5 minutos
Detailed view of programming code in a dark theme on a computer screen.

Los programas concurrentes y las operaciones por lotes pueden producir varias fallas al mismo tiempo. Antes, el código debía elegir una excepción, encadenar errores manualmente o devolver una lista. ExceptionGroup permite lanzar múltiples excepciones conservando su estructura, y la sintaxis except* trata selectivamente tipos concretos dentro del grupo.

Esta guía explica creación de grupos, tracebacks, tratamiento parcial, grupos anidados, integración con asyncio.TaskGroup, filtros, logging, compatibilidad de APIs y pruebas confiables.

Primer ExceptionGroup

errores = [
    ValueError("edad inválida"),
    KeyError("email"),
    RuntimeError("servicio no disponible"),
]

raise ExceptionGroup("fallas de procesamiento", errores)

El traceback muestra el contexto externo y cada excepción hija. El mensaje principal explica la operación mayor.

Por qué no devolver una lista

Una lista obliga a cada consumidor a recordar que debe revisarla y no se integra con el flujo normal de excepciones. Lanzar solo el primer error pierde información. ExceptionGroup conserva las fallas dentro del mecanismo estándar y permite tratarlas por tipo.

Tratar con except*

try:
    raise ExceptionGroup(
        "lote",
        [ValueError("A"), TypeError("B"), ValueError("C")],
    )
except* ValueError as grupo:
    for error in grupo.exceptions:
        print("valor inválido:", error)
except* TypeError as grupo:
    for error in grupo.exceptions:
        print("tipo inválido:", error)

Cada bloque except* recibe un subgrupo con las excepciones compatibles y conserva el anidamiento relevante.

except y except* son diferentes

except ExceptionGroup captura el objeto grupo completo. except* busca tipos dentro de él.

try:
    ejecutar_lote()
except ExceptionGroup as grupo:
    print("hijas directas:", len(grupo.exceptions))

Captura el grupo entero para logging, transformación o inspección manual. Usa except* para categorías.

Tratamiento parcial

Las excepciones coincidentes se consideran tratadas; las demás continúan propagándose.

try:
    raise ExceptionGroup(
        "operaciones",
        [ValueError("entrada"), OSError("disco")],
    )
except* ValueError:
    print("corrigiendo entrada")

El OSError no tratado vuelve a lanzarse dentro de un grupo. Así, un handler no oculta fallas que no sabe resolver.

Grupos anidados

Un ExceptionGroup puede contener otros grupos.

grupo = ExceptionGroup(
    "aplicación",
    [
        ExceptionGroup(
            "validación",
            [ValueError("nombre"), ValueError("email")],
        ),
        ExceptionGroup(
            "infraestructura",
            [TimeoutError("API"), OSError("archivo")],
        ),
    ],
)

La estructura puede representar subsistemas, tareas o etapas. except* conserva esa forma al seleccionar coincidencias.

ExceptionGroup y TaskGroup

asyncio.TaskGroup puede reunir fallas de tareas hijas en un ExceptionGroup.

import asyncio

async def fallar_valor():
    await asyncio.sleep(0)
    raise ValueError("respuesta inválida")

async def fallar_io():
    await asyncio.sleep(0)
    raise OSError("conexión perdida")

async def main():
    try:
        async with asyncio.TaskGroup() as grupo:
            grupo.create_task(fallar_valor())
            grupo.create_task(fallar_io())
    except* ValueError as errores:
        print("validación:", errores)
    except* OSError as errores:
        print("infraestructura:", errores)

Como la primera falla puede cancelar a las hermanas, no todas las tareas necesariamente llegan a lanzar su propia excepción. El grupo representa las fallas observadas durante el cierre.

BaseExceptionGroup

BaseExceptionGroup puede contener excepciones derivadas directamente de BaseException, como KeyboardInterrupt y SystemExit. ExceptionGroup acepta solo instancias de Exception.

La mayoría de aplicaciones debe crear ExceptionGroup. Las interrupciones del sistema tienen una semántica especial.

Crear grupos solo cuando sea necesario

def validar_registros(registros: list[dict]) -> None:
    errores: list[Exception] = []

    for indice, registro in enumerate(registros):
        try:
            validar_registro(registro)
        except ValueError as exc:
            exc.add_note(f"registro en índice {indice}")
            errores.append(exc)

    if errores:
        raise ExceptionGroup("registros inválidos", errores)

add_note() añade contexto individual sin reemplazar el mensaje original.

Notas en excepciones

try:
    convertir(valor)
except ValueError as exc:
    exc.add_note(f"campo: {campo}")
    exc.add_note(f"archivo: {archivo}")
    raise

En lotes, incluye índice, identificador, ruta o tarea. Evita datos sensibles.

Filtrar con subgroup()

subgroup() selecciona excepciones que cumplen una condición.

solo_io = grupo.subgroup(lambda error: isinstance(error, OSError))
if solo_io is not None:
    print(solo_io)

El resultado conserva las ramas del grupo original donde existen coincidencias.

Separar con split()

split() devuelve coincidencias y resto.

io, otros = grupo.split(OSError)

if io is not None:
    registrar_io(io)
if otros is not None:
    raise otros

Es útil en middlewares o bibliotecas que tratan una categoría y preservan el resto.

derive() y subclases

Una biblioteca puede crear una subclase de ExceptionGroup para transportar metadatos. Las operaciones de filtrado pueden llamar a derive() para conservarla.

class ErroresLote(ExceptionGroup):
    def __new__(cls, mensaje, excepciones, lote_id):
        obj = super().__new__(cls, mensaje, excepciones)
        obj.lote_id = lote_id
        return obj

    def derive(self, excepciones):
        return ErroresLote(self.message, excepciones, self.lote_id)

El código de aplicación normal casi nunca necesita una subclase.

Los subgrupos capturados son temporales

El objeto recibido por except* es un subgrupo para ese handler. Modificar sus atributos no reescribe automáticamente el grupo que continuará propagándose. Añade contexto a las excepciones individuales o lanza un nuevo error con chaining explícito.

Lanzar nuevos errores dentro de except*

try:
    ejecutar()
except* ValueError as errores:
    raise RuntimeError("falló la validación del lote") from errores

Los errores nuevos y las excepciones originales no tratadas se combinan según las reglas del lenguaje. Conserva la causa rica en lugar de reemplazarla por un mensaje genérico.

Logging de grupos

El logging tradicional puede producir tracebacks largos, pero útiles. Mantén la excepción completa para diagnóstico y genera métricas agregadas por tipo.

from collections import Counter


def contar_tipos(grupo: BaseExceptionGroup) -> Counter[str]:
    conteo: Counter[str] = Counter()

    def visitar(exc: BaseException) -> None:
        if isinstance(exc, BaseExceptionGroup):
            for hija in exc.exceptions:
                visitar(hija)
        else:
            conteo[type(exc).__name__] += 1

    visitar(grupo)
    return conteo

No descartes la estructura original, porque puede revelar qué subtarea produjo cada error.

Compatibilidad de APIs públicas

Una función que antes lanzaba un solo ValueError y empieza a lanzar ExceptionGroup cambió su contrato. Los consumidores con except ValueError no tratarán automáticamente un ValueError contenido en un grupo.

Documenta el cambio, usa versionado y considera un modo fail-fast cuando la compatibilidad importe.

Fail fast o reunir todos

Reunir todas las fallas es útil en formularios, migraciones, compilación y auditorías. Fail fast es mejor cuando continuar es costoso, peligroso o no aporta información.

ExceptionGroup no obliga a recopilar; ofrece una representación adecuada cuando varias fallas realmente deben comunicarse.

Probar ExceptionGroup

Pytest permite capturar e inspeccionar el grupo.

import pytest


def test_validacion_lote():
    with pytest.raises(ExceptionGroup) as captura:
        validar_registros([{}, {}])

    grupo = captura.value
    assert grupo.message == "registros inválidos"
    assert len(grupo.exceptions) == 2
    assert all(isinstance(e, ValueError) for e in grupo.exceptions)

Para grupos anidados, verifica tipos, notas y estructura relevante, no el texto completo del traceback.

Compatibilidad de versión

ExceptionGroup y except* pertenecen a Python moderno. Proyectos con versiones anteriores pueden usar el backport exceptiongroup para el objeto, pero la sintaxis except* requiere soporte del lenguaje.

Declara la versión mínima y verifica linters, type checkers, test runners y cobertura.

Errores comunes

  • Capturar solo Exception: no trata selectivamente tipos internos.
  • Ocultar el resto: las fallas no resueltas deben continuar.
  • Aplanar grupos sin necesidad: conserva la estructura.
  • Usar un grupo para cada falla única: una excepción normal puede ser más clara.
  • Recopilar cuando continuar es inseguro: decide entre lote y fail fast.
  • Cambiar el contrato sin documentar: los consumidores deben conocer el modelo.

Ejemplo completo: importación por lotes

from pathlib import Path


def importar_archivo(ruta: Path) -> list[dict]:
    resultados: list[dict] = []
    errores: list[Exception] = []

    for numero, linea in enumerate(ruta.read_text().splitlines(), start=1):
        try:
            resultados.append(parsear_linea(linea))
        except (ValueError, KeyError) as exc:
            exc.add_note(f"línea {numero}")
            exc.add_note(f"archivo {ruta.name}")
            errores.append(exc)

    if errores:
        raise ExceptionGroup(
            f"falló la importación de {ruta.name}",
            errores,
        )

    return resultados

try:
    importar_archivo(Path("clientes.txt"))
except* ValueError as errores:
    print("valores inválidos:", len(errores.exceptions))
except* KeyError as errores:
    print("campos ausentes:", len(errores.exceptions))

El consumidor recibe todas las fallas relevantes con línea y archivo, y puede tratar categorías de forma independiente.

Conclusión

ExceptionGroup representa múltiples fallas sin perder estructura, mientras except* permite tratamiento selectivo. Es especialmente útil en concurrencia estructurada, validación y procesamiento por lotes.

La documentación oficial de ExceptionGroup en Python y la referencia de except* explican las reglas. Usa grupos cuando varias fallas importen, conserva los errores no tratados y añade suficiente contexto para diagnosticar.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Picturesque wooden boardwalk leading to a serene beach under clear blue skies.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Path.walk en Python: recorre directorios

    Aprende Path.walk en Python para recorrer directorios, podar carpetas, manejar errores y symlinks, calcular tamaños y soportar versiones antiguas.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A man in a blue shirt holding a wall clock above his head, contemplating time.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    asyncio.timeout en Python: controla plazos

    Aprende asyncio.timeout en Python para deadlines, timeout_at, reagendamiento, TaskGroup, cleanup, retries y cancelación asíncrona segura.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Detailed view of a resting reticulated python showcasing its textured scales and intricate patterns.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    TaskGroup en Python: concurrencia estructurada

    Aprende asyncio.TaskGroup en Python para concurrencia estructurada, resultados, cancelación, ExceptionGroup, timeouts y grupos anidados.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A monochrome image of a lens on an open dictionary page, highlighting words.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    MappingProxyType: diccionario solo lectura

    Aprende MappingProxyType en Python para exponer diccionarios de solo lectura, crear vistas dinámicas y snapshots, y proteger invariantes sin copias.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Retro fuel pump with rusty metal and vintage design, featuring a nozzle and liter meter.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    Literal en Python: restringe valores

    Aprende typing.Literal en Python para restringir valores, crear overloads, discriminar TypedDict, usar match/case y mejorar APIs tipadas.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    TypedDict en Python: diccionarios tipados

    Aprende TypedDict en Python para diccionarios tipados, claves opcionales, NotRequired, Required, payloads de APIs y variantes discriminadas.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026