Programas concorrentes e operações em lote podem produzir várias falhas ao mesmo tempo. Antes, o código precisava escolher uma exceção, encadear erros manualmente ou guardar falhas em listas. ExceptionGroup permite levantar múltiplas exceções preservando sua estrutura, enquanto a sintaxe except* trata seletivamente tipos específicos dentro do grupo.
Neste guia, você aprenderá a criar grupos, entender tracebacks, usar except*, tratar apenas parte das falhas, trabalhar com grupos aninhados, integrar com asyncio.TaskGroup, filtrar exceções, registrar erros e escrever testes confiáveis.
Primeiro ExceptionGroup
erros = [
ValueError("idade inválida"),
KeyError("email"),
RuntimeError("serviço indisponível"),
]
raise ExceptionGroup("falhas no processamento", erros)O traceback mostra o grupo e cada exceção filha. A mensagem principal fornece contexto sobre a operação maior.
Por que uma lista de erros não basta?
Retornar uma lista exige que todo consumidor lembre de verificá-la e não integra com o fluxo normal de exceções. Levantar apenas o primeiro erro perde informações. ExceptionGroup mantém as falhas dentro do mecanismo de exceções e permite tratamento por tipo.
Tratando com except*
try:
raise ExceptionGroup(
"lote",
[ValueError("A"), TypeError("B"), ValueError("C")],
)
except* ValueError as grupo:
for erro in grupo.exceptions:
print("valor inválido:", erro)
except* TypeError as grupo:
for erro in grupo.exceptions:
print("tipo inválido:", erro)Cada bloco except* recebe um subgrupo contendo apenas exceções compatíveis, preservando a estrutura necessária.
except e except* não são iguais
except ExceptionGroup captura o grupo inteiro como um objeto. except* procura tipos dentro do grupo.
try:
executar_lote()
except ExceptionGroup as grupo:
print("quantidade direta:", len(grupo.exceptions))Use a captura do grupo inteiro para logging, transformação ou propagação. Use except* quando deseja tratar categorias de falha.
Tratamento parcial
Exceções correspondentes são tratadas; as restantes continuam propagando.
try:
raise ExceptionGroup(
"operações",
[ValueError("entrada"), OSError("disco")],
)
except* ValueError:
print("corrigindo entrada")
O OSError não tratado é relançado em um grupo. Isso evita esconder falhas que o bloco não sabe resolver.
Grupos aninhados
ExceptionGroups podem conter outros grupos.
grupo = ExceptionGroup(
"aplicação",
[
ExceptionGroup(
"validação",
[ValueError("nome"), ValueError("email")],
),
ExceptionGroup(
"infraestrutura",
[TimeoutError("API"), OSError("arquivo")],
),
],
)A estrutura pode representar subsistemas, tarefas ou etapas. except* preserva o formato ao selecionar exceções compatíveis.
ExceptionGroup e TaskGroup
asyncio.TaskGroup pode reunir falhas de várias tarefas em um ExceptionGroup.
import asyncio
async def falhar_valor():
await asyncio.sleep(0)
raise ValueError("resposta inválida")
async def falhar_io():
await asyncio.sleep(0)
raise OSError("conexão perdida")
async def main():
try:
async with asyncio.TaskGroup() as grupo:
grupo.create_task(falhar_valor())
grupo.create_task(falhar_io())
except* ValueError as erros:
print("validação:", erros)
except* OSError as erros:
print("infraestrutura:", erros)Dependendo do momento da primeira falha e do cancelamento das tarefas irmãs, nem toda tarefa necessariamente chega a lançar sua exceção. O grupo representa as falhas realmente observadas durante o encerramento.
BaseExceptionGroup
BaseExceptionGroup pode conter exceções que derivam diretamente de BaseException, como KeyboardInterrupt e SystemExit. ExceptionGroup aceita apenas instâncias de Exception.
Na maioria das aplicações, crie ExceptionGroup. Interrupções de sistema possuem semântica especial e não devem ser tratadas como erros comuns.
Construindo grupos apenas quando necessário
def validar_registros(registros: list[dict]) -> None:
erros: list[Exception] = []
for indice, registro in enumerate(registros):
try:
validar_registro(registro)
except ValueError as exc:
exc.add_note(f"registro na posição {indice}")
erros.append(exc)
if erros:
raise ExceptionGroup("registros inválidos", erros)add_note() adiciona contexto individual sem substituir a mensagem original.
Notas em exceções
Notas aparecem no traceback e ajudam a identificar a origem.
try:
converter(valor)
except ValueError as exc:
exc.add_note(f"campo: {campo}")
exc.add_note(f"arquivo: {arquivo}")
raiseEm lotes, inclua índice, ID, caminho ou tarefa. Evite dados sensíveis.
Filtrando com subgroup()
Um grupo oferece subgroup() para selecionar exceções que satisfazem uma condição.
somente_io = grupo.subgroup(lambda erro: isinstance(erro, OSError))
if somente_io is not None:
print(somente_io)O resultado mantém a estrutura do grupo original onde houver correspondências.
Separando com split()
split() devolve uma dupla: correspondências e restante.
io, outros = grupo.split(OSError)
if io is not None:
registrar_io(io)
if outros is not None:
raise outrosÉ uma ferramenta útil em middlewares ou bibliotecas que tratam um subconjunto e preservam o restante.
derive() e subclasses
Bibliotecas podem criar subclasses de ExceptionGroup para transportar metadados adicionais. Ao filtrar grupos, a implementação pode sobrescrever derive() para preservar a subclasse.
class ErrosDeLote(ExceptionGroup):
def __new__(cls, mensagem, excecoes, lote_id):
obj = super().__new__(cls, mensagem, excecoes)
obj.lote_id = lote_id
return obj
def derive(self, excecoes):
return ErrosDeLote(self.message, excecoes, self.lote_id)Para código de aplicação comum, o tipo padrão costuma ser suficiente.
Não modificar o grupo capturado
O objeto recebido por except* representa um subgrupo efêmero. Alterar atributos nele não modifica automaticamente o grupo que será propagado. Para adicionar contexto, trabalhe nas exceções individuais ou levante uma nova exceção com encadeamento explícito.
Levantando novos erros em except*
try:
executar()
except* ValueError as erros:
raise RuntimeError("falha de validação do lote") from errosOs novos erros e as exceções não tratadas são combinados conforme as regras da linguagem. Mantenha o contexto claro e evite transformar grupos ricos em uma mensagem genérica sem causa.
Logging de grupos
Logging tradicional pode produzir tracebacks extensos. Registre a exceção completa para diagnóstico e extraia métricas agregadas por tipo.
from collections import Counter
def contar(grupo: BaseExceptionGroup) -> Counter[str]:
contagem: Counter[str] = Counter()
def visitar(exc: BaseException) -> None:
if isinstance(exc, BaseExceptionGroup):
for filha in exc.exceptions:
visitar(filha)
else:
contagem[type(exc).__name__] += 1
visitar(grupo)
return contagemNão remova a estrutura original do log, pois ela pode revelar qual subtarefa produziu cada erro.
APIs e compatibilidade
Uma função que antes levantava uma única ValueError e passa a levantar ExceptionGroup altera o contrato. Consumidores com except ValueError não tratarão automaticamente o valor dentro do grupo.
Documente a mudança, use versionamento e considere oferecer um modo “fail fast” quando a compatibilidade for importante.
Fail fast ou coletar todos?
Coletar todas as falhas é útil em validação de formulários, migrações, compilação e auditorias. Fail fast é melhor quando continuar é caro, perigoso ou não produz informação adicional.
ExceptionGroup não obriga a coletar. Ele oferece uma representação adequada quando várias falhas realmente precisam ser comunicadas.
Testando ExceptionGroup
Com pytest, você pode capturar o grupo e inspecionar suas exceções.
import pytest
def test_validacao_em_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 estruturas aninhadas, teste tipos e contexto relevantes, não a formatação completa do traceback.
Compatibilidade de versão
ExceptionGroup e except* fazem parte de versões modernas do Python. Projetos compatíveis com versões anteriores podem usar o pacote backport exceptiongroup para o objeto, mas a sintaxe except* exige suporte da linguagem.
Defina a versão mínima e verifique ferramentas de lint, type checking e cobertura.
Erros comuns
- Capturar apenas Exception: isso não trata tipos internos do grupo seletivamente.
- Engolir o restante: deixe exceções não resolvidas propagarem.
- Achatar grupos sem necessidade: preserve a estrutura para diagnóstico.
- Usar grupos para uma única falha sempre: uma exceção normal pode ser mais simples.
- Coletar erros quando continuar é inseguro: escolha conscientemente entre lote e fail fast.
- Quebrar contratos sem documentar: consumidores precisam conhecer o novo modelo.
Exemplo completo: importação em lote
from pathlib import Path
class ErroLinha(ValueError):
pass
def importar_arquivo(caminho: Path) -> list[dict]:
resultados: list[dict] = []
erros: list[Exception] = []
for numero, linha in enumerate(caminho.read_text().splitlines(), start=1):
try:
resultados.append(parsear_linha(linha))
except (ValueError, KeyError) as exc:
exc.add_note(f"linha {numero}")
exc.add_note(f"arquivo {caminho.name}")
erros.append(exc)
if erros:
raise ExceptionGroup(
f"falhas ao importar {caminho.name}",
erros,
)
return resultados
try:
importar_arquivo(Path("clientes.txt"))
except* ValueError as erros:
print("valores inválidos:", len(erros.exceptions))
except* KeyError as erros:
print("campos ausentes:", len(erros.exceptions))O chamador recebe todas as falhas relevantes, com número da linha e arquivo, e pode tratar categorias separadamente.
Conclusão
ExceptionGroup representa múltiplas falhas sem perder a estrutura, enquanto except* permite tratamento seletivo. O recurso é especialmente importante em concorrência estruturada, validações e operações em lote.
A documentação oficial de ExceptionGroup no Python e a referência de except* detalham as regras. Use grupos quando várias falhas importam, preserve erros não tratados e mantenha contexto suficiente para diagnóstico.







