ExceptionGroup no Python: múltiplos erros

Publicado em: 28/08/2026
Tempo de leitura: 5 minutos
Yellow block letters spelling 'error' on a vibrant pink background, capturing a playful message.

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}")
    raise

Em 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 erros

Os 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 contagem

Nã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.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Conteúdo do artigo

    Artigos relacionados

    A tranquil wooden pathway winds through a vibrant autumn forest, covered in fallen leaves.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Path.walk no Python: percorra diretórios

    Aprenda Path.walk no Python para percorrer diretórios, podar pastas, tratar erros, links simbólicos, tamanhos, remoção segura e compatibilidade.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Minimalist hourglass filled with sand symbolizing time and patience, against a soft background.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    asyncio.timeout no Python: controle prazos

    Aprenda asyncio.timeout no Python para deadlines, timeout_at, reagendamento, TaskGroup, cleanup, retries e cancelamento assíncrono seguro.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    A detailed view of computer programming code on a screen, showcasing software development.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    TaskGroup no Python: concorrência estruturada

    Aprenda asyncio.TaskGroup no Python para concorrência estruturada, resultados, cancelamento, ExceptionGroup, timeouts e tarefas aninhadas.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026
    Black and white close-up of a dictionary page showing the definition of 'virus.'
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    MappingProxyType: dicionário só leitura

    Aprenda MappingProxyType no Python para expor dicionários somente leitura, criar visões dinâmicas, snapshots e proteger invariantes sem cópias.

    Ler mais

    Tempo de leitura: 6 minutos
    28/08/2026
    A close-up of a padlock securing a wire fence, symbolizing protection and safety.
    Python Avançado
    Foto de perfil de Leandro Hirt da Academify

    Literal no Python: restrinja valores

    Aprenda typing.Literal no Python para restringir valores, criar overloads, discriminar TypedDict, usar match/case e melhorar APIs tipadas.

    Ler mais

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

    TypedDict no Python: dicionários tipados

    Aprenda TypedDict no Python para definir dicionários tipados, campos opcionais, NotRequired, Required, APIs e variantes com segurança estática.

    Ler mais

    Tempo de leitura: 5 minutos
    28/08/2026