Criando exceções customizadas
1. Por que criar exceções customizadas?
1.1. Limitações das exceções nativas do Python
Python oferece um conjunto robusto de exceções nativas como ValueError, TypeError, KeyError e RuntimeError. No entanto, em aplicações complexas, essas exceções genéricas não são suficientes para expressar nuances do domínio do problema. Por exemplo, um ValueError pode ser lançado por diversos motivos em diferentes partes do sistema, tornando difícil identificar a causa raiz apenas pelo tipo da exceção.
1.2. Clareza semântica: nomes que refletem o domínio do problema
Exceções customizadas permitem que você nomeie os erros de acordo com o contexto da sua aplicação. Em vez de ValueError("saldo insuficiente"), você pode ter SaldoInsuficienteError, que comunica imediatamente a natureza do problema, sem necessidade de ler a mensagem.
1.3. Facilidade de tratamento seletivo em blocos except
Com exceções customizadas, você pode capturar erros específicos de forma precisa:
try:
processar_pagamento(conta, valor)
except SaldoInsuficienteError:
notificar_usuario("Saldo insuficiente para realizar a operação.")
except LimiteDiarioExcedidoError:
notificar_usuario("Você excedeu o limite diário de transações.")
2. Estrutura básica de uma exceção customizada
2.1. Herdando da classe Exception (ou subclasses)
A maneira mais simples de criar uma exceção customizada é herdar da classe Exception:
class MeuErro(Exception):
pass
2.2. Exemplo mínimo: class MinhaExcecao(Exception): pass
class SaldoInsuficienteError(Exception):
pass
# Uso
try:
raise SaldoInsuficienteError("Saldo insuficiente para saque de R$ 500,00")
except SaldoInsuficienteError as e:
print(e) # Saída: Saldo insuficiente para saque de R$ 500,00
2.3. Diferença entre herdar de Exception vs BaseException
A hierarquia de exceções em Python é:
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
├── ... (todas as exceções comuns)
└── SuaExcecaoCustomizada
Herde sempre de Exception ou de suas subclasses, nunca diretamente de BaseException. Capturar BaseException pode suprimir SystemExit e KeyboardInterrupt, impedindo o encerramento adequado do programa.
3. Adicionando atributos e mensagens personalizadas
3.1. Sobrescrevendo o método __init__
class ErroDeAutenticacao(Exception):
def __init__(self, usuario, mensagem="Falha na autenticação"):
self.usuario = usuario
self.mensagem = mensagem
super().__init__(self.mensagem)
3.2. Armazenando dados extras (códigos de erro, valores inválidos)
class ErroDeValidacao(Exception):
def __init__(self, campo, valor, mensagem=None):
self.campo = campo
self.valor = valor
self.mensagem = mensagem or f"Campo '{campo}' com valor inválido: {valor}"
super().__init__(self.mensagem)
# Uso
raise ErroDeValidacao("email", "invalido@")
3.3. Personalizando a representação com __str__ e __repr__
class ErroDeAPI(Exception):
def __init__(self, status_code, mensagem):
self.status_code = status_code
self.mensagem = mensagem
super().__init__(self.mensagem)
def __str__(self):
return f"[{self.status_code}] {self.mensagem}"
def __repr__(self):
return f"ErroDeAPI(status_code={self.status_code!r}, mensagem={self.mensagem!r})"
# Uso
erro = ErroDeAPI(404, "Recurso não encontrado")
print(str(erro)) # [404] Recurso não encontrado
print(repr(erro)) # ErroDeAPI(status_code=404, mensagem='Recurso não encontrado')
4. Hierarquia de exceções customizadas
4.1. Criando exceções base para um módulo ou pacote
# modulo_excecoes.py
class ErroDoSistema(Exception):
"""Exceção base para todos os erros do sistema de pagamentos."""
pass
4.2. Exceções específicas herdando da exceção base
class ErroDeValidacao(ErroDoSistema):
pass
class CampoObrigatorio(ErroDeValidacao):
def __init__(self, campo):
self.campo = campo
super().__init__(f"O campo '{campo}' é obrigatório")
class ValorInvalido(ErroDeValidacao):
def __init__(self, campo, valor):
self.campo = campo
self.valor = valor
super().__init__(f"Valor '{valor}' inválido para o campo '{campo}'")
4.3. Exemplo prático: ErroDeValidacao → CampoObrigatorio, ValorInvalido
def validar_usuario(dados):
if "nome" not in dados or not dados["nome"]:
raise CampoObrigatorio("nome")
if "email" in dados and "@" not in dados["email"]:
raise ValorInvalido("email", dados["email"])
return dados
try:
validar_usuario({"email": "invalido"})
except CampoObrigatorio as e:
print(f"Campo obrigatório: {e.campo}")
except ValorInvalido as e:
print(f"Valor inválido: {e.valor} para campo {e.campo}")
except ErroDeValidacao as e:
print(f"Erro de validação: {e}")
5. Boas práticas de design
5.1. Nomenclatura: sufixo Error ou Exception?
A convenção em Python é usar o sufixo Error para exceções que representam erros, e Exception apenas quando o nome já é amplamente conhecido (como em bibliotecas específicas). Prefira SaldoInsuficienteError a SaldoInsuficienteException.
5.2. Documentação com docstrings e type hints
class ErroDeConexao(Exception):
"""Exceção lançada quando a conexão com o servidor falha.
Attributes:
host: Nome do host que falhou
porta: Número da porta utilizada
causa_original: Exceção original que causou o erro
"""
def __init__(self, host: str, porta: int, causa_original: Exception = None):
self.host = host
self.porta = porta
self.causa_original = causa_original
mensagem = f"Falha ao conectar em {host}:{porta}"
super().__init__(mensagem)
5.3. Evitando herança múltipla desnecessária
Herança múltipla em exceções pode complicar o tratamento. Use com moderação e apenas quando fizer sentido semântico:
# Evite isso a menos que seja realmente necessário
class ErroCritico(ErroDoSistema, KeyboardInterrupt):
pass
6. Integração com tratamento de exceções existente
6.1. Relançando exceções customizadas a partir de exceções nativas
def converter_moeda(valor, taxa):
try:
return float(valor) * float(taxa)
except (ValueError, TypeError) as e:
raise ErroDeConversao(f"Falha na conversão: {e}")
6.2. Uso de raise ... from para encadeamento de exceções
def processar_pagamento(conta, valor):
try:
conta.debitar(valor)
except ValueError as e:
raise SaldoInsuficienteError(
f"Saldo insuficiente para debitar R$ {valor:.2f}"
) from e
O encadeamento com from preserva a pilha de chamadas original, facilitando o debugging.
6.3. Exemplo: convertendo ValueError em SaldoInsuficienteError
class ContaBancaria:
def __init__(self, saldo=0):
self.saldo = saldo
def debitar(self, valor):
if valor <= 0:
raise ValueError("Valor deve ser positivo")
if valor > self.saldo:
raise ValueError("Saldo insuficiente")
self.saldo -= valor
def transferir(conta_origem, conta_destino, valor):
try:
conta_origem.debitar(valor)
conta_destino.creditar(valor)
except ValueError as e:
if "Saldo insuficiente" in str(e):
raise SaldoInsuficienteError(
f"Saldo disponível: R$ {conta_origem.saldo:.2f}"
) from e
raise
7. Exceções customizadas em projetos reais
7.1. Em APIs: retornando exceções para o framework web
# Flask
@app.errorhandler(ErroDeValidacao)
def handle_validacao(error):
return {"erro": str(error), "campo": error.campo}, 400
# FastAPI
from fastapi import HTTPException
class RecursoNaoEncontradoError(Exception):
def __init__(self, recurso_id):
self.recurso_id = recurso_id
super().__init__(f"Recurso {recurso_id} não encontrado")
@app.get("/usuarios/{usuario_id}")
def get_usuario(usuario_id: int):
usuario = buscar_usuario(usuario_id)
if not usuario:
raise RecursoNaoEncontradoError(usuario_id)
return usuario
7.2. Em bibliotecas: documentando exceções na interface pública
class MinhaBiblioteca:
"""API pública da biblioteca.
Raises:
ErroDeConexao: Se não for possível conectar ao servidor
ErroDeAutenticacao: Se as credenciais forem inválidas
ErroDeTimeout: Se a requisição exceder o tempo limite
"""
pass
7.3. Testando exceções customizadas com pytest.raises
import pytest
def test_validacao_campo_obrigatorio():
with pytest.raises(CampoObrigatorio) as exc_info:
validar_usuario({})
assert exc_info.value.campo == "nome"
assert "nome" in str(exc_info.value)
def test_hierarquia_excecoes():
with pytest.raises(ErroDoSistema):
raise CampoObrigatorio("email")
with pytest.raises(ErroDeValidacao):
raise ValorInvalido("idade", -1)
def test_encadeamento_excecoes():
with pytest.raises(SaldoInsuficienteError) as exc_info:
transferir(ContaBancaria(100), ContaBancaria(0), 200)
assert exc_info.value.__cause__ is not None
assert isinstance(exc_info.value.__cause__, ValueError)
Referências
- Documentação oficial: Exceções customizadas — Guia oficial do Python sobre como criar e usar exceções definidas pelo usuário
- PEP 8: Guia de estilo para nomenclatura de exceções — Recomendações de nomenclatura para classes de exceção no estilo Python
- Real Python: Custom Exceptions in Python — Tutorial completo sobre criação e uso de exceções customizadas com exemplos práticos
- Python Docs:
raisestatement e encadeamento — Documentação detalhada sobre a declaraçãoraisee o encadeamento comfrom - Pytest: Testando exceções com
pytest.raises— Guia oficial do pytest para testar exceções customizadas - Stack Overflow: Best practices for custom exceptions — Discussão sobre melhores práticas na declaração de exceções customizadas em Python moderno