Voltar ao Glossario
Glossario Python

Exceções em Python: O que São e Como Funcionam | Python Brasil

Exceções em Python: hierarquia, exceções personalizadas, exception groups (3.11), encadeamento, módulo warnings e boas práticas para projetos grandes.

O que são Exceções?

Exceções são eventos que ocorrem durante a execução de um programa e interrompem o fluxo normal de instruções. Em Python, erros em tempo de execução são representados como objetos de exceção — instâncias de classes que herdam de BaseException. A linguagem oferece um mecanismo robusto para tratar, propagar e criar esses erros de forma estruturada.

Diferente de linguagens como C, onde erros são frequentemente representados por códigos de retorno, Python abraça o uso de exceções como parte central do fluxo de controle. O princípio é: falhe alto e rápido, para que erros sejam detectados cedo e de forma explícita.

Tipos comuns de exceções

# TypeError: operação aplicada a tipo incompatível
"2" + 2           # TypeError: can only concatenate str (not "int") to str

# ValueError: tipo correto, mas valor inválido para a operação
int("abc")        # ValueError: invalid literal for int() with base 10: 'abc'
float("nan")      # Funciona: retorna float('nan')

# KeyError: chave não encontrada no dicionário
d = {'a': 1}
d['b']            # KeyError: 'b'

# IndexError: índice fora do alcance da sequência
lista = [1, 2, 3]
lista[10]         # IndexError: list index out of range

# AttributeError: objeto não tem o atributo ou método
"texto".metodo_inexistente()  # AttributeError

# FileNotFoundError: arquivo ou diretório não encontrado
open("nao_existe.txt")        # FileNotFoundError

# ZeroDivisionError
10 / 0            # ZeroDivisionError: division by zero

# StopIteration: sinaliza fim de iterador
gen = (x for x in [])
next(gen)         # StopIteration

# RecursionError: profundidade máxima de recursão excedida
def infinita(): infinita()
infinita()        # RecursionError: maximum recursion depth exceeded

Hierarquia de exceções

Todas as exceções herdam de BaseException. Entender essa hierarquia é fundamental para capturar as exceções certas:

BaseException
├── KeyboardInterrupt       # Ctrl+C pressionado pelo usuário
├── SystemExit              # sys.exit() chamado
├── GeneratorExit           # Gerador encerrado
└── Exception               # Base para exceções "normais"
    ├── ArithmeticError
    │   ├── ZeroDivisionError
    │   ├── OverflowError
    │   └── FloatingPointError
    ├── LookupError
    │   ├── KeyError
    │   └── IndexError
    ├── OSError (IOError, EnvironmentError)
    │   ├── FileNotFoundError
    │   ├── PermissionError
    │   ├── TimeoutError
    │   └── ConnectionError
    │       ├── ConnectionRefusedError
    │       └── ConnectionResetError
    ├── ValueError
    ├── TypeError
    ├── AttributeError
    ├── RuntimeError
    │   └── RecursionError
    └── StopIteration

Criando exceções personalizadas

Exceções personalizadas tornam seu código mais expressivo e facilita o tratamento de erros no nível correto de abstração:

# Hierarquia para um sistema bancário
class ErroSistema(Exception):
    """Exceção base para todos os erros do sistema bancário."""
    pass

class ErroConta(ErroSistema):
    """Erros relacionados a operações em contas."""
    def __init__(self, numero_conta: str, mensagem: str):
        self.numero_conta = numero_conta
        super().__init__(f"Conta {numero_conta}: {mensagem}")

class SaldoInsuficienteError(ErroConta):
    def __init__(self, numero_conta: str, saldo: float, valor: float):
        self.saldo = saldo
        self.valor = valor
        super().__init__(
            numero_conta,
            f"saldo R$ {saldo:.2f} insuficiente para sacar R$ {valor:.2f}"
        )

class ContaBloqueadaError(ErroConta):
    def __init__(self, numero_conta: str, motivo: str):
        self.motivo = motivo
        super().__init__(numero_conta, f"conta bloqueada: {motivo}")

class LimiteTransferenciaError(ErroConta):
    pass


class ContaBancaria:
    def __init__(self, numero: str, saldo: float):
        self.numero = numero
        self.saldo = saldo
        self.bloqueada = False

    def sacar(self, valor: float) -> None:
        if self.bloqueada:
            raise ContaBloqueadaError(self.numero, "suspeita de fraude")
        if valor > self.saldo:
            raise SaldoInsuficienteError(self.numero, self.saldo, valor)
        self.saldo -= valor

# Capturando em diferentes níveis de granularidade
conta = ContaBancaria("001-1", 100.0)
try:
    conta.sacar(200.0)
except SaldoInsuficienteError as e:
    print(f"Saldo insuficiente: R$ {e.saldo:.2f}")
except ContaBloqueadaError as e:
    print(f"Conta bloqueada: {e.motivo}")
except ErroConta as e:
    print(f"Erro genérico na conta: {e}")
except ErroSistema:
    print("Erro interno do sistema")

Encadeamento de exceções (raise from)

O encadeamento permite preservar o contexto original de um erro ao relançar uma exceção de nível mais alto:

import json

class ErroCarregarConfig(Exception):
    pass

def carregar_config(caminho: str) -> dict:
    try:
        with open(caminho) as f:
            return json.load(f)
    except FileNotFoundError as e:
        # raise X from Y: vincula explicitamente as exceções
        raise ErroCarregarConfig(f"Arquivo de configuração não encontrado: {caminho}") from e
    except json.JSONDecodeError as e:
        raise ErroCarregarConfig(f"JSON inválido em {caminho}: {e.msg}") from e

try:
    config = carregar_config("config.json")
except ErroCarregarConfig as e:
    print(f"Falha na inicialização: {e}")
    print(f"Causa original: {e.__cause__}")  # Acesso à exceção original

# raise X from None: suprime o contexto original (evita mensagens confusas)
def buscar_usuario(user_id: int):
    try:
        return banco.query(user_id)
    except DatabaseError as e:
        raise UsuarioNaoEncontrado(user_id) from None  # Não expõe detalhes internos

Notas em exceções (Python 3.11+)

O Python 3.11 introduziu o método add_note() para adicionar informações contextuais a exceções existentes:

# Python 3.11+
def processar_item(item: dict, indice: int):
    try:
        valor = float(item['preco'])
        resultado = calcular(valor)
    except KeyError as e:
        e.add_note(f"Chave ausente no item de índice {indice}")
        e.add_note(f"Item completo: {item}")
        raise
    except ValueError as e:
        e.add_note(f"Valor inválido no campo 'preco' do item {indice}: {item.get('preco')!r}")
        raise

# A nota aparece no traceback junto com a exceção original

Exception Groups (Python 3.11+)

ExceptionGroup permite agrupar e tratar múltiplas exceções simultaneamente. É especialmente útil em código assíncrono e paralelo:

# Python 3.11+
import asyncio

async def tarefa_com_falha(nome: str):
    if nome == "tarefa_1":
        raise ValueError("Valor inválido na tarefa 1")
    if nome == "tarefa_2":
        raise ConnectionError("Falha de conexão na tarefa 2")
    return f"{nome} concluída"

async def executar_tarefas():
    async with asyncio.TaskGroup() as tg:
        tg.create_task(tarefa_com_falha("tarefa_1"))
        tg.create_task(tarefa_com_falha("tarefa_2"))
        tg.create_task(tarefa_com_falha("tarefa_3"))
    # TaskGroup lança ExceptionGroup com todas as exceções

# Capturando com except*
try:
    asyncio.run(executar_tarefas())
except* ValueError as eg:
    print(f"Erros de valor: {eg.exceptions}")
except* ConnectionError as eg:
    print(f"Erros de conexão: {eg.exceptions}")

# Criando ExceptionGroup manualmente
erros = []
for i, item in enumerate(lista_de_itens):
    try:
        processar(item)
    except (ValueError, TypeError) as e:
        erros.append(e)

if erros:
    raise ExceptionGroup("Erros no processamento em lote", erros)

Módulo warnings

O módulo warnings é para situações que não são erros fatais, mas que merecem atenção do desenvolvedor:

import warnings

def funcao_obsoleta(x):
    warnings.warn(
        "funcao_obsoleta() foi descontinuada. Use nova_funcao() no lugar.",
        DeprecationWarning,
        stacklevel=2  # Aponta para quem chamou, não para esta função
    )
    return nova_funcao(x)

def funcao_com_dado_suspeito(valor: float) -> float:
    if valor < 0:
        warnings.warn(
            f"Valor negativo ({valor}) pode produzir resultados inesperados.",
            RuntimeWarning,
            stacklevel=2
        )
    return valor ** 0.5

# Controlando warnings
warnings.filterwarnings('ignore', category=DeprecationWarning)  # Silencia
warnings.filterwarnings('error', category=RuntimeWarning)       # Transforma em exceção

# No pytest, configure em pytest.ini:
# [pytest]
# filterwarnings =
#     error
#     ignore::DeprecationWarning:modulo_antigo

Registrando exceções corretamente

import logging

logger = logging.getLogger(__name__)

def processar_dados(dados: list) -> list:
    resultados = []
    for item in dados:
        try:
            resultado = operacao_arriscada(item)
            resultados.append(resultado)
        except ValueError as e:
            # logging.exception inclui o traceback completo automaticamente
            logger.exception("Falha ao processar item %r", item)
            # Não relança: continua processando os demais itens
        except Exception as e:
            logger.error(
                "Erro inesperado ao processar item %r: %s",
                item, e,
                exc_info=True  # Equivalente ao logging.exception
            )
            raise  # Erros inesperados devem ser propagados

    return resultados

Anti-padrões a evitar

# RUIM: capturando BaseException (inclui KeyboardInterrupt, SystemExit)
try:
    processar()
except BaseException:  # Nunca faça isso
    pass

# RUIM: silenciando exceções sem log
try:
    operacao()
except Exception:
    pass  # O que aconteceu? Mistério.

# RUIM: capturando Exception para re-raise sem adicionar contexto
try:
    operacao()
except Exception as e:
    raise e  # Perde o traceback original. Use apenas 'raise'

# BOM: raise sem argumento preserva o traceback completo
try:
    operacao()
except Exception:
    logger.exception("Falha na operação")
    raise  # Re-lança com traceback original intacto

# RUIM: usar exceções para controle de fluxo normal
# (o retorno None seria mais adequado aqui)
try:
    return dicionario[chave]
except KeyError:
    return valor_padrao  # Use: dicionario.get(chave, valor_padrao)

Performance de exceções

Em Python, o custo de um bloco try é quase zero quando nenhuma exceção é lançada. O custo real está na criação e propagação da exceção. Portanto, o estilo EAFP (pedir perdão depois) é eficiente quando a operação com sucesso é o caso comum. Se exceções são lançadas com alta frequência (ex: em loops muito apertados), o custo pode ser significativo — nesse caso, prefira checagens prévias (LBYL).

Termos Relacionados

  • Try/Except - Sintaxe para tratamento de exceções
  • Classe - Exceções são classes Python
  • Python - A linguagem de programação