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