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}")
raiseEn 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 otrosEs ú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 erroresLos 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 conteoNo 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.







