Exceções: try, except, finally e raise

1. Introdução ao Tratamento de Exceções em Python

Exceções são eventos que ocorrem durante a execução de um programa e interrompem o fluxo normal de instruções. Diferentemente de erros de sintaxe, que impedem o programa de sequer ser executado, as exceções acontecem em tempo de execução e podem ser tratadas para evitar que o programa seja abruptamente encerrado.

Em Python, toda exceção é uma classe que herda de BaseException. A hierarquia principal inclui:

  • BaseException — classe base para todas as exceções
  • Exception — classe base para exceções que não devem ser ignoradas (herda de BaseException)
  • ArithmeticError, LookupError, ValueError, TypeError, etc. — subclasses de Exception

Tratar exceções permite que você controle como seu programa reage a situações inesperadas, como arquivos inexistentes, divisões por zero ou entradas inválidas do usuário.

2. O Bloco try e except: Capturando Exceções

A estrutura mais básica para tratamento de exceções utiliza try e except:

try:
    numero = int(input("Digite um número: "))
    resultado = 10 / numero
    print(f"Resultado: {resultado}")
except ValueError:
    print("Erro: você não digitou um número válido!")
except ZeroDivisionError:
    print("Erro: não é possível dividir por zero!")

Você pode capturar múltiplas exceções em um único bloco except usando uma tupla:

try:
    arquivo = open("dados.txt", "r")
    conteudo = arquivo.read()
    numero = int(conteudo)
except (FileNotFoundError, ValueError) as erro:
    print(f"Erro ao processar arquivo: {erro}")

3. Especificando o Tipo de Exceção e Acessando Detalhes

Para acessar informações detalhadas sobre a exceção, utilize a sintaxe as:

try:
    lista = [1, 2, 3]
    print(lista[5])
except IndexError as e:
    print(f"Tipo do erro: {type(e).__name__}")
    print(f"Mensagem: {e}")
    print(f"Argumentos: {e.args}")

Evite usar except: sem especificar o tipo de exceção, pois isso captura até mesmo KeyboardInterrupt e SystemExit, o que pode mascarar problemas graves e dificultar a depuração.

# Ruim: captura tudo, inclusive erros que deveriam interromper o programa
try:
    operacao_arriscada()
except:
    print("Algo deu errado")

# Bom: captura apenas exceções específicas
try:
    operacao_arriscada()
except (ValueError, TypeError) as e:
    print(f"Erro esperado: {e}")

4. O Bloco else: Executando Código Quando Não Há Exceção

O bloco else é executado apenas quando nenhuma exceção ocorre no bloco try. Isso ajuda a separar o código que pode gerar exceções do código que deve rodar apenas em caso de sucesso:

try:
    idade = int(input("Digite sua idade: "))
except ValueError:
    print("Idade inválida!")
else:
    print(f"Você tem {idade} anos.")
    if idade >= 18:
        print("Você é maior de idade.")

Colocar código no else em vez de no try evita capturar exceções inesperadas que poderiam ocorrer em operações que você não pretendia proteger.

5. O Bloco finally: Garantindo Limpeza e Finalização

O bloco finally é executado sempre, independentemente de ocorrer ou não uma exceção. É o local ideal para liberar recursos:

arquivo = None
try:
    arquivo = open("relatorio.txt", "w")
    arquivo.write("Dados importantes")
    # Simulando um erro
    resultado = 1 / 0
except ZeroDivisionError:
    print("Erro matemático durante a escrita!")
finally:
    if arquivo:
        arquivo.close()
        print("Arquivo fechado com sucesso.")

A ordem de execução é: tryexcept (se houver exceção) ou else (se não houver) → finally.

6. Levantando Exceções com raise

Você pode levantar exceções intencionalmente com raise:

def dividir(a, b):
    if b == 0:
        raise ValueError("O divisor não pode ser zero!")
    return a / b

try:
    resultado = dividir(10, 0)
except ValueError as erro:
    print(f"Erro: {erro}")

Para relançar uma exceção capturada, use raise sem argumentos:

try:
    numero = int(input("Digite um número positivo: "))
    if numero < 0:
        raise ValueError("Número negativo não permitido!")
except ValueError as erro:
    print(f"Erro capturado: {erro}")
    raise  # Relança a exceção para ser tratada em nível superior

Para criar exceções personalizadas, herde de Exception:

class SaldoInsuficienteError(Exception):
    def __init__(self, saldo, valor_saque):
        self.saldo = saldo
        self.valor_saque = valor_saque
        super().__init__(f"Saldo insuficiente: R${saldo:.2f} para saque de R${valor_saque:.2f}")

def sacar(conta, valor):
    if valor > conta["saldo"]:
        raise SaldoInsuficienteError(conta["saldo"], valor)
    conta["saldo"] -= valor
    return conta["saldo"]

7. Combinando try, except, else, finally e raise

Um exemplo completo que demonstra a interação entre todos os blocos:

def processar_dados(arquivo_nome):
    dados = None
    try:
        with open(arquivo_nome, "r") as arquivo:
            dados = arquivo.read()
        if not dados.strip():
            raise ValueError("Arquivo vazio!")
        numeros = [int(x) for x in dados.split(",")]
    except FileNotFoundError:
        print(f"Arquivo '{arquivo_nome}' não encontrado.")
        return
    except ValueError as e:
        print(f"Erro nos dados: {e}")
        return
    else:
        media = sum(numeros) / len(numeros)
        print(f"Média dos números: {media:.2f}")
        return numeros
    finally:
        print("Processamento finalizado.")

# Testando diferentes cenários
print("=== Cenário 1: Arquivo inexistente ===")
processar_dados("inexistente.txt")

print("\n=== Cenário 2: Dados inválidos ===")
# Supondo que 'dados_invalidos.txt' contenha "1,2,abc,4"
processar_dados("dados_invalidos.txt")

print("\n=== Cenário 3: Sucesso ===")
# Supondo que 'dados_validos.txt' contenha "10,20,30,40"
processar_dados("dados_validos.txt")

Neste exemplo:
- Se o arquivo não existe, FileNotFoundError é capturado e a função retorna
- Se os dados são inválidos, ValueError é capturado
- Se tudo ocorre bem, o bloco else calcula a média
- O bloco finally sempre executa, indicando o fim do processamento

8. Erros Comuns e Dicas Finais

Erro 1: Capturar exceções genéricas

# Ruim
try:
    resultado = operacao_complexa()
except:
    pass  # Erros importantes são ignorados

# Bom
try:
    resultado = operacao_complexa()
except (ValueError, TypeError) as e:
    logger.error(f"Erro esperado: {e}")

Erro 2: Não liberar recursos adequadamente

# Ruim: arquivo pode não ser fechado se ocorrer exceção
arquivo = open("dados.txt", "r")
dados = arquivo.read()
if not dados:
    raise ValueError("Arquivo vazio")
arquivo.close()

# Bom: usando finally ou gerenciador de contexto
with open("dados.txt", "r") as arquivo:
    dados = arquivo.read()
    if not dados:
        raise ValueError("Arquivo vazio")
# Arquivo é fechado automaticamente

Erro 3: Aninhamento excessivo de blocos try/except

# Ruim: difícil de ler e manter
try:
    try:
        arquivo = open("dados.txt")
        try:
            numero = int(arquivo.read())
        except ValueError:
            print("Erro de conversão")
    except FileNotFoundError:
        print("Arquivo não encontrado")
finally:
    arquivo.close()

# Bom: blocos enxutos e separados
def ler_numero_do_arquivo(nome_arquivo):
    with open(nome_arquivo, "r") as arquivo:
        return int(arquivo.read())

try:
    numero = ler_numero_do_arquivo("dados.txt")
except FileNotFoundError:
    print("Arquivo não encontrado")
except ValueError:
    print("Erro de conversão")

Use raise quando a função não puder tratar adequadamente o erro e precisar delegar a responsabilidade para quem a chamou. Trate localmente apenas quando você puder oferecer uma alternativa viável ou uma mensagem de erro significativa para o usuário.

Referências