Python & AI Tutorials Logo
Programação Python

41. Debugging e Testes no Seu Código

Escrever código é só metade da batalha. A outra metade é garantir que seu código funciona corretamente e encontrar problemas quando ele não funciona. Todo programador, de iniciantes a especialistas, escreve código com bugs. A diferença é que programadores experientes desenvolveram abordagens sistemáticas para encontrar e corrigir esses bugs.

Neste capítulo, você vai aprender técnicas práticas de debugging que ajudam você a entender o que seu código está realmente fazendo, localizar problemas rapidamente e verificar se seu código funciona como deveria. Essas habilidades vão tornar você um programador mais confiante e eficaz.

41.1) Lendo Tracebacks para Localizar Erros (Revisão Rápida)

Como aprendemos no Capítulo 24, o Python fornece mensagens de erro detalhadas chamadas tracebacks quando algo dá errado. Vamos revisar como lê-las de forma eficiente, já que essa é sua primeira linha de defesa quando está fazendo debugging.

41.1.1) A Anatomia de um Traceback

Quando o Python encontra um erro, ele mostra exatamente onde o problema ocorreu e qual foi o tipo de erro. Aqui está um traceback típico:

python
def calculate_average(numbers):
    total = sum(numbers)
    count = len(numbers)
    return total / count
 
def process_student_grades(grades):
    average = calculate_average(grades)
    return f"Average: {average:.1f}"
 
# Isso vai causar um erro
student_grades = []
result = process_student_grades(student_grades)
print(result)

Saída:

Traceback (most recent call last):
  File "grades.py", line 12, in <module>
    result = process_student_grades(student_grades)
  File "grades.py", line 7, in process_student_grades
    average = calculate_average(grades)
  File "grades.py", line 4, in calculate_average
    return total / count
           ~~~~~~^~~~~~~
ZeroDivisionError: division by zero

Vamos detalhar o que esse traceback está nos dizendo:

Linha 12: process_student_grades chamada

Linha 7: calculate_average chamada

Linha 4: Operação de divisão

ZeroDivisionError: division by zero

Lendo de baixo para cima:

  1. O tipo de erro e a mensagem (em baixo): ZeroDivisionError: division by zero nos diz exatamente o que deu errado
  2. A linha exata onde o erro ocorreu: return total / count na linha 4
  3. A cadeia de chamadas mostrando como chegamos lá: começou na linha 12, passou pela linha 7, terminou na linha 4

41.1.2) Usando Tracebacks para Encontrar a Causa Raiz

O traceback mostra o sintoma (onde o erro ocorreu), mas você precisa encontrar a causa (por que ocorreu). Vamos rastrear o problema:

python
# O erro acontece aqui
return total / count  # count é 0
 
# Mas o problema real é aqui
student_grades = []  # Lista vazia passada para a função

A divisão por zero acontece porque passamos uma lista vazia. O traceback aponta para a linha 4, mas a correção precisa acontecer antes — seja validando a entrada ou tratando o caso de lista vazia:

python
def calculate_average(numbers):
    """Return the average of numbers, or None if the list is empty."""
    if not numbers:
        return None
    return sum(numbers) / len(numbers)
 
def process_student_grades(grades):
    """Process student grades and return a formatted string."""
    average = calculate_average(grades)
    if average is None:
        return "No grades to process"
    return f"Average: {average:.1f}"
 
# Agora isso funciona com segurança
student_grades = []
result = process_student_grades(student_grades)
print(result)  # Output: No grades to process
 
# E isso também funciona
student_grades = [85, 92, 78, 90]
result = process_student_grades(student_grades)
print(result)  # Output: Average: 86.2

Pontos-chave:

  • Leia tracebacks de baixo para cima
  • A localização do erro (sintoma) nem sempre é a causa raiz
  • Valide entradas cedo para prevenir erros depois
  • Use programação defensiva (.get(), verificações de tamanho) para um código mais seguro

Tipos diferentes de erros produzem tracebacks diferentes, mas o processo de leitura é sempre o mesmo: comece de baixo para ver o que deu errado e, depois, suba para entender como você chegou lá. Se você precisar relembrar tipos específicos de exceção, volte ao Capítulo 24.

Agora que você consegue ler tracebacks de forma eficiente, vamos aprender como percorrer seu código mentalmente para entender o que ele está fazendo passo a passo.

41.2) Rastreando a Execução do Código Mentalmente

Às vezes, você encontra um bug mas não consegue rodar o código imediatamente — talvez você esteja revisando código no papel, lendo o pull request de outra pessoa ou tentando entender por que uma função se comporta de forma inesperada. Nessas situações, a execução mental (mental execution) — passar pelo código linha por linha na sua cabeça, acompanhando o que acontece com cada variável — se torna inestimável.

Até programadores experientes usam essa técnica regularmente. Antes de adicionar prints ou rodar um debugger, eles muitas vezes fazem algumas iterações mentalmente para formar uma hipótese sobre onde o problema pode estar. Isso é mais rápido do que tentativa e erro e ajuda você a entender seu código de forma mais profunda.

A execução mental é particularmente útil quando:

  • Lendo código desconhecido para entender o que ele faz
  • Revisando funções pequenas (5-15 linhas) antes de rodá-las
  • Depurando erros de lógica onde o código roda, mas produz resultados errados
  • Entendendo o comportamento de um loop quando o padrão não é óbvio de imediato
  • Code review onde você não consegue rodar o código facilmente por conta própria

Para códigos maiores ou mais complexos, você vai combinar o rastreamento mental com outras técnicas que veremos mais adiante neste capítulo. Mas dominar essa habilidade vai tornar você um debugger muito mais eficaz.

41.2.1) O Processo de Execução Mental

Quando você executa código mentalmente, você age como o interpretador do Python, seguindo as mesmas regras que o Python segue. Vamos praticar com um exemplo simples:

python
def find_maximum(numbers):
    max_value = numbers[0]
    for num in numbers:
        if num > max_value:
            max_value = num
    return max_value
 
result = find_maximum([3, 7, 2, 9, 5])
print(result)  # Output: 9

Veja como rastrear esse código:

Rastreamento passo a passo:

Initial state:
  numbers = [3, 7, 2, 9, 5]
  max_value = 3  (numbers[0])
 
Iteration 1: num = 3
  Check: 3 > 3? → False
  max_value remains 3
 
Iteration 2: num = 7
  Check: 7 > 3? → True
  max_value = 7 ✓
 
Iteration 3: num = 2
  Check: 2 > 7? → False
  max_value remains 7
 
Iteration 4: num = 9
  Check: 9 > 7? → True
  max_value = 9 ✓
 
Iteration 5: num = 5
  Check: 5 > 9? → False
  max_value remains 9
 
Return: 9

41.2.2) Criando uma Tabela de Rastreamento

Para códigos mais complexos, crie uma tabela de rastreamento (trace table) que mostra como as variáveis mudam ao longo do tempo. Isso é especialmente útil para loops e estruturas aninhadas:

python
def calculate_running_totals(numbers):
    totals = []
    running_sum = 0
    for num in numbers:
        running_sum += num
        totals.append(running_sum)
    return totals
 
result = calculate_running_totals([10, 20, 30, 40])
print(result)  # Output: [10, 30, 60, 100]

Tabela de rastreamento:

A tabela mostra o estado das variáveis em cada passo. Note como running_sum muda de "antes" para "depois" a cada adição:

Iterationnumrunning_sum (before)running_sum (after)totals
Start-00[]
110010[10]
2201030[10, 30]
3303060[10, 30, 60]
44060100[10, 30, 60, 100]

Criar essa tabela ajuda você a ver exatamente como os dados fluem pelo seu código. Se a saída não bater com o que você espera, você consegue identificar exatamente onde as coisas deram errado.

41.2.3) Rastreando a Lógica Condicional

Instruções condicionais exigem atenção cuidadosa a quais ramificações executam. Vamos rastrear um exemplo mais complexo:

python
def categorize_grade(score):
    if score >= 90:
        category = "Excellent"
        bonus = 10
    elif score >= 80:
        category = "Good"
        bonus = 5
    elif score >= 70:
        category = "Satisfactory"
        bonus = 0
    else:
        category = "Needs Improvement"
        bonus = 0
    
    final_score = score + bonus
    return category, final_score
 
result = categorize_grade(85)
print(result)  # Output: ('Good', 90)

Rastreamento mental para score = 85:

  1. Verifique 85 >= 90 → False, pule o primeiro bloco
  2. Verifique 85 >= 80 → True, entre no segundo bloco
  3. Defina category = "Good" e bonus = 5
  4. Pule os blocos elif e else restantes (já encontrou uma correspondência)
  5. Calcule final_score = 85 + 5 = 90
  6. Retorne ("Good", 90)

41.2.4) Rastreando Chamadas e Retornos de Funções

Quando funções chamam outras funções, você precisa acompanhar a pilha de chamadas — a sequência de chamadas de função e suas variáveis locais:

python
def calculate_tax(amount, rate):
    tax = amount * rate
    return tax
 
def calculate_total(price, quantity, tax_rate):
    subtotal = price * quantity
    tax = calculate_tax(subtotal, tax_rate)
    total = subtotal + tax
    return total
 
result = calculate_total(50, 3, 0.08)
print(f"Total: ${result:.2f}")  # Output: Total: $162.00

Rastreamento com a pilha de chamadas:

┌─ calculate_total(50, 3, 0.08)
│  price = 50, quantity = 3, tax_rate = 0.08
│  subtotal = 150

│  ┌─ calculate_tax(150, 0.08)
│  │  amount = 150, rate = 0.08
│  │  tax = 12.0
│  │  return 12.0
│  └─

│  tax = 12.0 (from calculate_tax)
│  total = 162.0
│  return 162.0
└─
 
result = 162.0

Esse rastreamento passo a passo mostra exatamente como os dados fluem entre as funções. Ao fazer debugging, se o resultado final estiver errado, você pode rastrear de volta para ver qual função produziu um valor intermediário incorreto.

O rastreamento mental é poderoso, mas para código complexo pode ser cansativo. Na próxima seção, vamos aprender como usar prints de forma estratégica para ver o que está realmente acontecendo enquanto o código roda, o que muitas vezes é mais rápido e mais confiável do que apenas execução mental.

41.3) Debugging com Print: f"{var=}" e repr()

Embora a execução mental funcione bem para funções pequenas, ela fica impraticável para códigos maiores ou mais complexos. Quando você não tem certeza do que está acontecendo dentro de um loop, ou quando um cálculo produz resultados inesperados, o jeito mais rápido de investigar muitas vezes é adicionar print() de forma estratégica.

O print debugging tem algumas vantagens em relação a outras técnicas:

  • Nenhuma ferramenta especial necessária: Funciona em qualquer ambiente Python
  • Rápido de implementar: Adicione um print em segundos
  • Saída clara: Você vê exatamente o que pediu
  • Fácil de remover: Apague os prints quando terminar

Desenvolvedores profissionais usam print debugging o tempo todo — não é uma técnica "de iniciante". Vamos aprender como usá-la de forma eficiente.

41.3.1) Print Debugging Básico

A abordagem mais simples de debugging é imprimir os valores das variáveis em pontos-chave do seu código:

python
def process_order(items, discount_rate):
    print(f"Starting process_order")
    print(f"Items: {items}")
    print(f"Discount rate: {discount_rate}")
    
    subtotal = sum(item['price'] * item['quantity'] for item in items)
    print(f"Subtotal: {subtotal}")
    
    discount = subtotal * discount_rate
    print(f"Discount amount: {discount}")
    
    total = subtotal - discount
    print(f"Final total: {total}")
    
    return total
 
order_items = [
    {'name': 'Book', 'price': 25.99, 'quantity': 2},
    {'name': 'Pen', 'price': 3.50, 'quantity': 5}
]
 
result = process_order(order_items, 0.10)

Saída:

Starting process_order
Items: [{'name': 'Book', 'price': 25.99, 'quantity': 2}, {'name': 'Pen', 'price': 3.5, 'quantity': 5}]
Discount rate: 0.1
Subtotal: 69.47999999999999
Discount amount: 6.9479999999999995
Final total: 62.53199999999999

Esses prints mostram o fluxo de execução e os valores em cada passo. Se o resultado final estiver errado, você consegue ver exatamente onde o cálculo saiu do esperado.

41.3.2) Usando f"{var=}" para Inspeção Rápida

O Python 3.8 introduziu uma sintaxe conveniente para debugging: f"{var=}". Isso imprime tanto o nome da variável quanto o valor:

python
def calculate_compound_interest(principal, rate, years):
    # Abordagem tradicional
    print(f"principal: {principal}")
    print(f"rate: {rate}")
    print(f"years: {years}")
    
    # Abordagem mais limpa com f"{var=}"
    print(f"{principal=}")
    print(f"{rate=}")
    print(f"{years=}")
    
    # Você pode usar expressões, não só variáveis
    print(f"{principal * rate=}")
    print(f"{(1 + rate) ** years=}")
    
    amount = principal * (1 + rate) ** years
    print(f"{amount=}")
    
    return amount
 
result = calculate_compound_interest(1000, 0.05, 10)

Saída:

principal: 1000
rate: 0.05
years: 10
principal=1000
rate=0.05
years=10
principal * rate=50.0
(1 + rate) ** years=1.628894626777442
amount=1628.894626777442

41.3.3) Usando repr() para Ver a Forma Real dos Dados

Às vezes, o que você vê impresso não é o que você acha que é. A função repr() mostra a representação exata de um objeto, incluindo caracteres escondidos:

python
# Essas strings parecem iguais quando impressas
text1 = "Hello"
text2 = "Hello\n"  # Tem um newline no final
 
print("Using print():")
print(f"text1: {text1}")
print(f"text2: {text2}")
 
print("\nUsing repr():")
print(f"text1: {repr(text1)}")
print(f"text2: {repr(text2)}")

Saída:

Using print():
text1: Hello
text2: Hello
 
Using repr():
text1: 'Hello'
text2: 'Hello\n'

A saída de repr() mostra que text2 tem um caractere de newline escondido. Isso é crucial ao depurar processamento de strings:

python
def clean_user_input():
    # A entrada do usuário muitas vezes tem espaços em branco escondidos
    username = input("Enter username: ")  # Usuário digita "Alice  "
    
    print(f"Username with print(): {username}")
    print(f"Username with repr(): {repr(username)}")
    
    # Limpa a entrada
    cleaned = username.strip()
    print(f"Cleaned with repr(): {repr(cleaned)}")
    
    return cleaned

Se um usuário digitar "Alice" seguido de espaços e pressionar Enter, você pode ver:

Saída:

Enter username: Alice  
Username with print(): Alice  
Username with repr(): 'Alice  '
Cleaned with repr(): 'Alice'

A saída de repr() revela os espaços no final que o print() não mostra com clareza.

Quando usar repr() vs str():

repr() é feito para desenvolvedores — ele mostra a representação "oficial" da string que poderia recriar o objeto. str() (que o print() usa por padrão) é feito para usuários finais — ele mostra uma versão legível e amigável.

Para debugging, repr() geralmente é mais útil porque revela a estrutura real dos seus dados.

41.3.4) Posicionamento Estratégico de Prints

Não saia espalhando prints por todo lado. Coloque-os estrategicamente:

python
def calculate_shipping_cost(weight, distance, express=False):
    print(f"=== calculate_shipping_cost called ===")
    print(f"Input: {weight=}, {distance=}, {express=}")
    
    # Calcula o custo base
    base_rate = 0.50
    base_cost = weight * distance * base_rate
    print(f"Calculated: {base_cost=}")
    
    # Aplica taxa extra de envio expresso
    if express:
        surcharge = base_cost * 0.50
        print(f"Express surcharge: {surcharge=}")
        total = base_cost + surcharge
    else:
        print("No express surcharge")
        total = base_cost
    
    print(f"Final: {total=}")
    print(f"=== calculate_shipping_cost returning ===\n")
    return total
 
# Testa cenários diferentes
cost1 = calculate_shipping_cost(10, 500, express=True)
cost2 = calculate_shipping_cost(5, 200, express=False)

Saída:

=== calculate_shipping_cost called ===
Input: weight=10, distance=500, express=True
Calculated: base_cost=2500.0
Express surcharge: surcharge=1250.0
Final: total=3750.0
=== calculate_shipping_cost returning ===
 
=== calculate_shipping_cost called ===
Input: weight=5, distance=200, express=False
Calculated: base_cost=500.0
No express surcharge
Final: total=500.0
=== calculate_shipping_cost returning ===

Os marcadores claros (===) e a saída organizada facilitam acompanhar o fluxo de execução.

41.3.5) Removendo Prints de Debug

Depois que você encontrou e corrigiu o bug, lembre-se de remover seus prints de debug. Aqui vão algumas estratégias:

Estratégia 1: Use um prefixo distinto

python
# Fácil de encontrar e remover com search/replace
print(f"DEBUG: {total=}")
print(f"DEBUG: {items=}")

Estratégia 2: Use uma flag de debug

python
DEBUG = True
 
def calculate_total(items):
    if DEBUG:
        print(f"Processing {len(items)} items")
    
    total = sum(item['price'] for item in items)
    
    if DEBUG:
        print(f"{total=}")
    
    return total
 
# Desativa toda a saída de debug de uma vez
DEBUG = False

Estratégia 3: Comente eles, mas mantenha

python
def process_data(data):
    # print(f"DEBUG: {data=}")  # Útil para debugging no futuro
    result = transform(data)
    # print(f"DEBUG: {result=}")
    return result

Para um logging mais sofisticado que você pode deixar em código de produção, o Python tem um módulo logging, mas prints simples são perfeitos para debugging rápido durante o desenvolvimento.

O print debugging mostra os valores das variáveis, mas às vezes você precisa entender a estrutura de um objeto — que métodos ele tem, que tipo ele é e o que ele pode fazer. Na próxima seção, vamos aprender como inspecionar objetos usando type() e dir().

41.4) Inspecionando Objetos: type() e dir()

O print debugging mostra os valores das suas variáveis, mas às vezes o problema não é o valor — é o tipo de objeto com o qual você está trabalhando. Você pode esperar uma lista(list), mas receber uma string; ou pode estar trabalhando com um objeto desconhecido e não saber quais métodos ele suporta.

O Python oferece ferramentas embutidas para inspecionar objetos: type() diz que tipo de objeto você tem, e dir() mostra quais operações ele suporta. Essas funções são essenciais quando:

  • Fazendo debugging de erros relacionados a tipo (TypeError, AttributeError)
  • Trabalhando com bibliotecas ou APIs desconhecidas
  • Entendendo objetos retornados por código de terceiros
  • Verificando se seu código recebe os tipos esperados

Vamos aprender como usar essas ferramentas de inspeção de forma eficiente.

41.4.1) Usando type() para Identificar Tipos de Objetos

A função type() diz exatamente que tipo de objeto você tem. Isso é crucial ao fazer debugging de erros relacionados a tipo:

python
def process_data(data):
    print(f"Received data: {data}")
    print(f"Data type: {type(data)}")
    
    if isinstance(data, list):
        print("Processing as list")
        return sum(data)
    elif isinstance(data, dict):
        print("Processing as dictionary")
        return sum(data.values())
    else:
        print("Unexpected type!")
        return None
 
# Teste com tipos diferentes
result1 = process_data([10, 20, 30])
print(f"Result: {result1}\n")
 
result2 = process_data({'a': 10, 'b': 20, 'c': 30})
print(f"Result: {result2}\n")
 
result3 = process_data("123")
print(f"Result: {result3}")

Saída:

Received data: [10, 20, 30]
Data type: <class 'list'>
Processing as list
Result: 60
 
Received data: {'a': 10, 'b': 20, 'c': 30}
Data type: <class 'dict'>
Processing as dictionary
Result: 60
 
Received data: 123
Data type: <class 'str'>
Unexpected type!
Result: None

41.4.2) Depurando Confusão de Tipos

Confusão de tipos é uma fonte comum de bugs, especialmente ao trabalhar com funções que podem receber dados de múltiplas fontes — entrada do usuário, leitura de arquivo, respostas de API ou outras funções. Você pode esperar uma lista de números, mas receber uma string por engano, ou esperar um dicionário, mas obter uma lista.

Usar type() ajuda a identificar quando você está com o tipo errado. Ao imprimir o tipo cedo na sua função, você consegue detectar incompatibilidades de tipo imediatamente, antes que elas causem mensagens de erro confusas mais profundamente no seu código:

python
def calculate_average(numbers):
    print(f"{type(numbers)=}")
    print(f"{numbers=}")  # Mostra o que realmente recebemos
    
    # Isso vai falhar se numbers não for uma lista de números
    total = sum(numbers)
    count = len(numbers)
    return total / count
 
# Erro comum: esqueceu de converter string para lista
scores = "85"  # Deveria ser [85] ou apenas 85
try:
    avg = calculate_average(scores)
    print(f"Average: {avg}")
except TypeError as e:
    print(f"TypeError: {e}")
    print(f"Expected list of numbers, got {type(scores)}")
    print(f"The string contains: {repr(scores)}")

Saída:

type(numbers)=<class 'str'>
numbers='85'
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Expected list of numbers, got <class 'str'>
The string contains: '85'

A verificação com type() revela o problema imediatamente: passamos uma string quando precisávamos de uma lista. Sem essa saída de debug, você poderia ter gasto tempo tentando entender por que sum() falhou, quando a questão real é que o tipo errado de dado entrou na função em primeiro lugar.

41.4.3) Usando dir() para Descobrir Métodos Disponíveis

Ao trabalhar com objetos desconhecidos — seja de uma biblioteca que você está aprendendo, uma resposta de API ou até os tipos embutidos do Python — você frequentemente precisa saber: "O que eu posso fazer com esse objeto?" A função dir() responde a essa pergunta listando todos os atributos e métodos disponíveis em um objeto.

Isso é especialmente valioso quando:

  • Você está explorando uma nova biblioteca e quer ver quais métodos um objeto oferece
  • Você recebe um objeto de código de terceiros e precisa entender suas capacidades
  • Você esqueceu o nome exato de um método que quer usar
  • Você está fazendo debugging e quer verificar se um objeto tem os métodos que você espera

Vamos explorar quais métodos uma string tem:

python
# Explorando quais métodos uma string tem
text = "Python Programming"
 
print(f"Type: {type(text)}")
print(f"\nAvailable string methods (showing first 10):")
methods = [m for m in dir(text) if not m.startswith('_')]
for method in methods[:10]:  # Show first 10
    print(f"  {method}")
print(f"  ... and {len(methods) - 10} more")

Saída:

Type: <class 'str'>
 
Available string methods (showing first 10):
  capitalize
  casefold
  center
  count
  encode
  endswith
  expandtabs
  find
  format
  format_map
  ... and 37 more

Agora você consegue ver todas as operações disponíveis em strings. Se você não tinha certeza se strings tinham um método count ou um método endswith, o dir() mostra que eles existem. Você então pode usar a função help() do Python para aprender mais sobre qualquer método específico:

python
# Learn more about a specific method
help(text.count)

Isso vai mostrar a documentação do método count:

Help on built-in function count:
 
count(sub[, start[, end]], /) method of builtins.str instance
    Return the number of non-overlapping occurrences of substring sub in string S[start:end].
 
    Optional arguments start and end are interpreted as in slice notation.

A função dir() é como ter documentação integrada no Python — ela mostra o que é possível com qualquer objeto com o qual você esteja trabalhando.

41.4.4) Inspecionando Objetos Personalizados

Ao trabalhar com classes personalizadas, type() e dir() ajudam você a entender com o que está lidando. Além disso, o Python fornece hasattr() para verificar se um objeto tem um atributo específico antes de tentar acessá-lo — isso evita exceções AttributeError.

python
class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
    
    def get_status(self):
        return "Passing" if self.grade >= 60 else "Failing"
 
student = Student("Alice", 85)
 
print(f"Object type: {type(student)}")
print(f"\nAvailable attributes and methods:")
for attr in dir(student):
    if not attr.startswith('_'):
        print(f"  {attr}")
 
# Verifica se atributos específicos existem
print(f"\nHas 'name' attribute: {hasattr(student, 'name')}")
print(f"Has 'age' attribute: {hasattr(student, 'age')}")
print(f"Has 'get_status' method: {hasattr(student, 'get_status')}")
 
# Agora podemos acessar com segurança atributos que sabemos que existem
if hasattr(student, 'name'):
    print(f"\nStudent name: {student.name}")
else:
    print("\nNo name attribute found")
 
if hasattr(student, 'get_status'):
    print(f"Status: {student.get_status()}")
else:
    print("No get_status method found")
 
# Isso evita erros como este:
# print(student.age)  # Would raise AttributeError!

Saída:

Object type: <class '__main__.Student'>
 
Available attributes and methods:
  get_status
  grade
  name
 
Has 'name' attribute: True
Has 'age' attribute: False
Has 'get_status' method: True
 
Student name: Alice
Status: Passing

A função hasattr() é essencial para escrever código defensivo — código que verifica se operações são seguras antes de executá-las. A função retorna True se o atributo existe e False se não existe — permitindo que você tome decisões antes de tentar acessar atributos. Isso é especialmente importante ao trabalhar com objetos vindos de bibliotecas externas ou entrada do usuário, onde você não pode garantir quais atributos estarão presentes.

41.4.5) Usando getattr() para Acesso Seguro a Atributos

Quando você não tem certeza se um atributo existe, use getattr() com um valor padrão:

python
def display_student_info(student):
    """Safely display student info even if some attributes are missing."""
    print(f"Type: {type(student)}")
    
    # Acesso seguro a atributos com valores padrão
    name = getattr(student, 'name', 'Unknown')
    grade = getattr(student, 'grade', 0)
    age = getattr(student, 'age', 'Not specified')
    
    print(f"Name: {name}")
    print(f"Grade: {grade}")
    print(f"Age: {age}")
    
    # Verifica se o método existe antes de chamar
    if hasattr(student, 'get_status'):
        status = student.get_status()
        print(f"Status: {status}")
 
# Usando a mesma classe Student de cima
student = Student("Bob", 72)
display_student_info(student)

Saída:

Type: <class '__main__.Student'>
Name: Bob
Grade: 72
Age: Not specified
Status: Passing

Essa abordagem evita exceções AttributeError ao trabalhar com objetos que podem não ter todos os atributos esperados. A função getattr() é especialmente útil quando:

  • Trabalhando com objetos de APIs externas que podem ter versões diferentes
  • Lidando com atributos opcionais nas suas próprias classes
  • Construindo código defensivo que lida com dados ausentes de forma graciosa

Entender que tipo de objeto você tem e quais métodos ele suporta é crucial para debugging. Mas às vezes você precisa verificar não apenas se seu código roda, mas se ele produz os resultados corretos. Na próxima seção, vamos aprender como usar assert para testar suas suposições e capturar bugs cedo.

41.5) Testando com Instruções assert

Aprendemos como fazer debugging quando algo dá errado — lendo tracebacks, rastreando a execução mentalmente, usando prints e inspecionando objetos. Mas existe uma abordagem melhor do que corrigir bugs depois que eles aparecem: preveni-los em primeiro lugar por meio de testes.

A instrução assert é a ferramenta de teste mais simples do Python. Ela permite verificar se seu código se comporta corretamente, checando suposições em pontos críticos. Quando uma asserção falha, o Python diz imediatamente exatamente o que deu errado e onde, tornando muito mais fácil capturar bugs cedo — muitas vezes antes mesmo de você rodar seu programa principal.

Asserções são particularmente valiosas para:

  • Verificar se funções produzem resultados esperados
  • Checar se entradas atendem seus requisitos
  • Testar casos de borda que podem quebrar seu código
  • Documentar suposições das quais seu código depende

Pense em asserções como verificações automatizadas que validam continuamente se seu código está funcionando como deveria. Vamos aprender como usá-las de forma eficiente.

41.5.1) O Que assert Faz

Uma instrução assert verifica se uma condição é verdadeira. Se a condição for verdadeira, nada acontece — o código continua normalmente. Se for falsa, o Python levanta um AssertionError e para a execução.

Sintaxe:

python
assert condition, "Optional error message"
  • condition: Qualquer expressão que avalia para True ou False
  • "Optional error message": Texto útil mostrado quando a asserção falha

Veja como isso funciona na prática:

python
# Asserções simples
x = 10
assert x > 0  # Passa silenciosamente (x é de fato > 0)
assert x < 5  # Falha! Levanta AssertionError
 
# Com mensagens de erro (muito mais útil!)
assert x > 0, f"x must be positive, got {x}"
assert x < 5, f"x must be less than 5, got {x}"  # Falha com mensagem clara

Agora vamos ver asserções em uma função real:

python
def calculate_discount(price, discount_percent):
    # Verifica se as entradas são válidas
    assert price >= 0, "Price cannot be negative"
    assert 0 <= discount_percent <= 100, "Discount must be between 0 and 100"
    
    discount_amount = price * (discount_percent / 100)
    final_price = price - discount_amount
    
    # Verifica se a saída faz sentido
    assert final_price >= 0, "Final price cannot be negative"
    
    return final_price
 
# Entradas válidas funcionam normalmente
result = calculate_discount(100, 20)
print(f"Price after 20% discount: ${result}")  # Output: Price after 20% discount: $80.0
 
# Entradas inválidas disparam asserções
try:
    result = calculate_discount(-50, 20)
except AssertionError as e:
    print(f"Assertion failed: {e}")  # Output: Assertion failed: Price cannot be negative
 
try:
    result = calculate_discount(100, 150)
except AssertionError as e:
    print(f"Assertion failed: {e}")  # Output: Assertion failed: Discount must be between 0 and 100

41.5.2) Usando Asserções para Verificar o Comportamento de Funções

Asserções são excelentes para testar se funções produzem resultados esperados:

python
def calculate_average(numbers):
    if not numbers:
        return 0.0
    return sum(numbers) / len(numbers)
 
# Testa com várias entradas
result = calculate_average([10, 20, 30])
assert result == 20.0, f"Expected 20.0, got {result}"
print(f"Test 1 passed: average of [10, 20, 30] = {result}")
 
result = calculate_average([5, 5, 5, 5])
assert result == 5.0, f"Expected 5.0, got {result}"
print(f"Test 2 passed: average of [5, 5, 5, 5] = {result}")
 
result = calculate_average([])
assert result == 0.0, f"Expected 0.0 for empty list, got {result}"
print(f"Test 3 passed: average of [] = {result}")
 
result = calculate_average([100])
assert result == 100.0, f"Expected 100.0, got {result}"
print(f"Test 4 passed: average of [100] = {result}")

Saída:

Test 1 passed: average of [10, 20, 30] = 20.0
Test 2 passed: average of [5, 5, 5, 5] = 5.0
Test 3 passed: average of [] = 0.0
Test 4 passed: average of [100] = 100.0

Se qualquer asserção falhar, você sabe imediatamente qual caso de teste revelou o problema.

41.5.3) Testando Casos de Borda

Casos de borda são entradas nos limites do que sua função deve suportar. Testá-los revela bugs que entradas normais podem não mostrar:

python
def get_first_and_last(items):
    """Return the first and last items from a sequence."""
    assert len(items) > 0, "Cannot get first and last from empty sequence"
    return items[0], items[-1]
 
# Testa caso normal
result = get_first_and_last([1, 2, 3, 4, 5])
assert result == (1, 5), f"Expected (1, 5), got {result}"
print(f"Normal case: {result}")
 
# Testa caso de borda: item único
result = get_first_and_last([42])
assert result == (42, 42), f"Expected (42, 42), got {result}"
print(f"Single item: {result}")
 
# Testa caso de borda: dois itens
result = get_first_and_last([10, 20])
assert result == (10, 20), f"Expected (10, 20), got {result}"
print(f"Two items: {result}")
 
# Testa caso de borda: sequência vazia (deve falhar)
try:
    result = get_first_and_last([])
    print("ERROR: Should have raised AssertionError for empty list")
except AssertionError as e:
    print(f"Empty list correctly rejected: {e}")

Saída:

Normal case: (1, 5)
Single item: (42, 42)
Two items: (10, 20)
Empty list correctly rejected: Cannot get first and last from empty sequence

41.5.4) Testando Transformações de Dados

Quando sua função transforma dados, use assert para garantir que a transformação está correta:

python
def remove_duplicates(items):
    """Remove duplicates while preserving order."""
    seen = set()
    result = []
    for item in items:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result
 
# Testa remoção básica de duplicatas
input_data = [1, 2, 2, 3, 1, 4, 3, 5]
result = remove_duplicates(input_data)
expected = [1, 2, 3, 4, 5]
assert result == expected, f"Expected {expected}, got {result}"
print(f"Test 1 passed: {input_data} -> {result}")
 
# Testa se a ordem é preservada
input_data = [3, 1, 2, 1, 3, 2]
result = remove_duplicates(input_data)
expected = [3, 1, 2]
assert result == expected, f"Expected {expected}, got {result}"
print(f"Test 2 passed: {input_data} -> {result}")
 
# Testa sem duplicatas
input_data = [1, 2, 3, 4, 5]
result = remove_duplicates(input_data)
assert result == input_data, f"Expected {input_data}, got {result}"
print(f"Test 3 passed: {input_data} -> {result}")
 
# Testa com tudo duplicado
input_data = [5, 5, 5, 5]
result = remove_duplicates(input_data)
expected = [5]
assert result == expected, f"Expected {expected}, got {result}"
print(f"Test 4 passed: {input_data} -> {result}")

Saída:

Test 1 passed: [1, 2, 2, 3, 1, 4, 3, 5] -> [1, 2, 3, 4, 5]
Test 2 passed: [3, 1, 2, 1, 3, 2] -> [3, 1, 2]
Test 3 passed: [1, 2, 3, 4, 5] -> [1, 2, 3, 4, 5]
Test 4 passed: [5, 5, 5, 5] -> [5]

41.5.5) Criando uma Função de Teste Simples

Conforme seu código cresce, espalhar asserts pelo código principal fica bagunçado e difícil de gerenciar. Uma abordagem melhor é organizar seus testes em funções de teste dedicadas. Isso separa código de teste de código de produção e facilita rodar todos os testes de uma vez.

Por que usar funções de teste dedicadas?

  • Organização: Todos os testes de uma função ficam em um só lugar
  • Reutilização: Rode os testes sempre que mudar o código
  • Documentação: Testes mostram como a função deve se comportar
  • Debugging: Quando um teste falha, você sabe imediatamente qual cenário quebrou
  • Fluxo de desenvolvimento: Teste primeiro, depois implemente ou corrija o código

Vamos ver isso na prática:

python
def calculate_grade(score):
    """Convert numeric score to letter grade."""
    if score >= 90:
        return 'A'
    elif score >= 80:
        return 'B'
    elif score >= 70:
        return 'C'
    elif score >= 60:
        return 'D'
    else:
        return 'F'
 
def test_calculate_grade():
    """Test the calculate_grade function.
    
    This function tests all expected behaviors:
    - Each grade range (A, B, C, D, F)
    - Boundary values (90, 80, 70, 60)
    - Edge cases (just below each boundary)
    """
    print("Testing calculate_grade...")
    
    # Test A grades
    assert calculate_grade(95) == 'A', "95 should be A"
    assert calculate_grade(90) == 'A', "90 should be A (boundary)"
    print("  ✓ A grades: passed")
    
    # Test B grades
    assert calculate_grade(85) == 'B', "85 should be B"
    assert calculate_grade(80) == 'B', "80 should be B (boundary)"
    print("  ✓ B grades: passed")
    
    # Test C grades
    assert calculate_grade(75) == 'C', "75 should be C"
    assert calculate_grade(70) == 'C', "70 should be C (boundary)"
    print("  ✓ C grades: passed")
    
    # Test D grades
    assert calculate_grade(65) == 'D', "65 should be D"
    assert calculate_grade(60) == 'D', "60 should be D (boundary)"
    print("  ✓ D grades: passed")
    
    # Test F grades
    assert calculate_grade(55) == 'F', "55 should be F"
    assert calculate_grade(0) == 'F', "0 should be F"
    print("  ✓ F grades: passed")
    
    # Test boundary edge cases (one below each threshold)
    assert calculate_grade(89) == 'B', "89 should be B (just below A)"
    assert calculate_grade(79) == 'C', "79 should be C (just below B)"
    assert calculate_grade(69) == 'D', "69 should be D (just below C)"
    assert calculate_grade(59) == 'F', "59 should be F (just below D)"
    print("  ✓ Boundary cases: passed")
    
    print("All tests passed! ✓\n")
 
# Run the tests
test_calculate_grade()
 
# Now you can confidently use the function
student_score = 87
grade = calculate_grade(student_score)
print(f"Student score {student_score} = Grade {grade}")

Saída:

Testing calculate_grade...
  ✓ A grades: passed
  ✓ B grades: passed
  ✓ C grades: passed
  ✓ D grades: passed
  ✓ F grades: passed
  ✓ Boundary cases: passed
All tests passed! ✓
 
Student score 87 = Grade B

Benefícios dessa abordagem:

  1. Organização clara dos testes: Você consegue ver todos os casos de teste rapidamente
  2. Fácil de rodar: Basta chamar test_calculate_grade() sempre que modificar a função
  3. Feedback progressivo: Veja quais grupos de teste passam conforme a função roda
  4. Autoexplicativo: A função de teste mostra exatamente como calculate_grade() deve funcionar

Quando rodar seus testes:

  • Antes de fazer mudanças: Garanta que os testes passam com o código atual
  • Depois de fazer mudanças: Verifique se você não quebrou nada
  • Ao adicionar funcionalidades: Escreva testes para a nova feature primeiro (test-driven development)
  • Ao corrigir bugs: Adicione um teste que reproduz o bug e depois corrija

Esse padrão simples — escrever funções de teste com asserts — é a base de testes profissionais de software. Conforme você evoluir, vai aprender sobre frameworks de teste como pytest e unittest, mas a ideia central continua a mesma: escrever funções que verificam se seu código funciona corretamente.

41.5.6) Quando Usar Asserções vs Exceções

Entender quando usar asserções versus exceções é crucial. Elas servem a propósitos fundamentalmente diferentes:

Asserções são para encontrar bugs durante o desenvolvimento:

  • Elas checam coisas que nunca deveriam ser falsas se seu código estiver escrito corretamente
  • Elas verificam suposições internas e a lógica do seu próprio código
  • Elas ajudam você a capturar erros de programação enquanto escreve e testa o código
  • Exemplo: "Neste ponto da minha função, esta lista nunca deveria estar vazia"
  • Exemplo: "Todos os itens nesta lista deveriam ser inteiros porque eu acabei de filtrá-los"

Exceções são para lidar com erros que podem acontecer durante a operação normal:

  • Elas lidam com condições externas que você não consegue controlar
  • Elas tratam situações que podem ocorrer mesmo quando seu código está perfeito
  • Elas permitem que seu programa se recupere com elegância ou falhe de forma informativa
  • Exemplo: Usuário digita texto quando você esperava um número
  • Exemplo: Um arquivo que seu código tenta abrir não existe
  • Exemplo: Requisição de rede expira por timeout

A diferença principal: asserções dizem "isso deveria ser impossível", enquanto exceções dizem "isso pode acontecer, e aqui está como vamos lidar com isso".

Vamos ver isso na prática:

python
# Exemplo 1: Função usada com ENTRADA DO USUÁRIO
# Usuários podem digitar qualquer coisa, incluindo 0
def calculate_user_ratio(numerator, denominator):
    """Calculate ratio from user-provided numbers."""
    # Usuário pode digitar 0, então use tratamento de exceção
    if denominator == 0:
        raise ValueError("Denominator cannot be zero")
    
    return numerator / denominator
 
# Exemplo 2: Cálculo interno onde 0 deveria ser impossível
def calculate_percentage(part, total):
    """Calculate what percentage 'part' is of 'total'."""
    # Isso é chamado internamente depois que verificamos total > 0
    # Se total for 0, é um bug de programação no nosso código
    assert total > 0, "total must be positive - check calling code"
    
    return (part / total) * 100

Mais exemplos do que cada um deve tratar:

SituaçãoUse AsserçãoUse Exceção
Usuário digita entrada inválida❌ Não✅ Sim
Arquivo não existe❌ Não✅ Sim
Requisição de rede falha❌ Não✅ Sim
Função recebe tipo de parâmetro errado do seu código✅ Sim❌ Não
Lista deveria ter itens mas está vazia por erro de lógica✅ Sim❌ Não
Estrutura de dados em estado inesperado por bug✅ Sim❌ Não
Conexão com banco de dados falha❌ Não✅ Sim
API retorna formato inesperado❌ Não✅ Sim
Seu algoritmo produz resultado matematicamente impossível✅ Sim❌ Não

Limitação crítica das asserções:

Asserções podem ser completamente desativadas quando o Python roda com otimização:

bash
python -O script.py  # All assert statements are ignored!

Quando asserções são desativadas, elas simplesmente desaparecem — o Python não as verifica. Isso significa:

  • Nunca use asserções para validar entrada do usuário
  • Nunca use asserções para verificações de segurança
  • Nunca use asserções para nada que precisa sempre funcionar em produção
python
# PERIGOSO - NÃO FAÇA ISSO:
def process_payment(amount):
    assert amount > 0, "Amount must be positive"  # ERRADO! É desativado com -O
    # Process payment...
 
# CORRETO - FAÇA ISSO:
def process_payment(amount):
    if amount <= 0:
        raise ValueError("Amount must be positive")  # Sempre verificado!
    # Process payment...

Em resumo:

  • Asserções = "Estou checando meu próprio código por bugs durante o desenvolvimento"

    • Pense: "Isso deveria ser impossível se eu codifiquei corretamente"
    • Elas ajudam você a encontrar erros na sua lógica
  • Exceções = "Estou lidando com condições do mundo real que podem acontecer"

    • Pense: "Isso pode acontecer durante o uso normal, e eu preciso lidar com isso"
    • Elas ajudam seu programa a lidar com situações imprevisíveis

Asserções são ferramentas de desenvolvimento e debugging que ajudam você a escrever código correto. Exceções são ferramentas de produção que ajudam seu programa a lidar com a realidade bagunçada de entrada do usuário, sistemas de arquivos, redes e outros fatores externos que você não consegue controlar.


Agora você aprendeu as técnicas essenciais de debugging e testes que vão servir você ao longo da sua jornada de programação:

  • Ler tracebacks para localizar rapidamente onde erros ocorrem
  • Rastrear código mentalmente para entender o que seu código faz passo a passo
  • Usar prints de forma estratégica para ver valores em runtime e o fluxo
  • Inspecionar objetos com type() e dir() para entender com o que você está trabalhando
  • Testar com asserções para verificar se seu código funciona e capturar bugs cedo

Essas habilidades trabalham juntas como um kit completo de debugging. Quando você encontrar um problema:

  1. Leia o traceback para descobrir onde falhou
  2. Use print debugging ou rastreamento mental para entender por quê
  3. Use inspeção com type/dir quando não tiver certeza do que um objeto pode fazer
  4. Escreva asserções para evitar que o bug volte

Com prática, você vai desenvolver uma intuição sobre qual técnica usar em cada situação. Lembre-se: todo programador faz debugging de código — a diferença é que programadores experientes fazem isso de forma sistemática e eficiente. Essas técnicas vão tornar você um deles.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai