Depurar código é uma habilidade fundamental para qualquer programador. Nesta aula, vamos mergulhar no mundo do debugging em Python, explorando desde o depurador interativo pdb até a moderna função breakpoint(), além de discutir a diferença entre usar print para depuração e a prática recomendada de logging. Ao final, você terá um arsenal de ferramentas para encontrar e corrigir bugs com muito mais eficiência.

O debugging não é apenas sobre corrigir erros; é sobre entender o comportamento do seu programa. Muitas vezes, o problema não está onde pensamos, e ferramentas como pdb nos permitem inspecionar o estado do programa em tempo de execução. Vamos começar com o básico e evoluir para técnicas mais avançadas.

pdb

O pdb (Python Debugger) é um módulo da biblioteca padrão que fornece um depurador interativo para Python. Ele permite que você pause a execução do programa em pontos específicos, inspecione variáveis, execute código passo a passo e até mesmo modifique o estado do programa enquanto ele está em execução. Isso é extremamente útil para entender o fluxo do programa e identificar onde algo está dando errado.

Para usar o pdb, você pode inseri-lo no código com import pdb; pdb.set_trace() ou usar a linha de comando com python -m pdb script.py. No entanto, a forma mais simples e moderna é usar a função breakpoint(), que discutiremos em seguida. Quando o pdb é acionado, você entra em um prompt interativo com comandos como n (next), s (step), c (continue), p (print), l (list) e q (quit).

Vamos a um exemplo prático. Suponha que temos um programa que calcula a média de uma lista, mas queremos entender por que está retornando um valor incorreto:

import pdb

def calcular_media(numeros):
    total = 0
    for i in range(len(numeros)):
        total += numeros[i]
        pdb.set_trace()  # Pausa a execução aqui
    return total / len(numeros)

notas = [7, 8, 9]
media = calcular_media(notas)
print(f"A média é {media}")

Ao executar o código, o programa pausará na primeira iteração, e você pode inspecionar i, total e numeros. Use n para avançar para a próxima linha, p total para imprimir o valor de total, e c para continuar até o próximo breakpoint ou até o final. Isso permite que você veja exatamente como os valores mudam a cada iteração.

breakpoint()

A função breakpoint() foi introduzida no Python 3.7 como uma forma mais simples e flexível de acionar o depurador. Por padrão, ela chama pdb.set_trace(), mas você pode alterar o comportamento definindo a variável de ambiente PYTHONBREAKPOINT. Por exemplo, se você definir PYTHONBREAKPOINT=0, a função breakpoint() não faz nada, o que é útil para desativar todos os breakpoints em produção sem precisar remover as chamadas.

Outra vantagem é que você pode substituir o depurador padrão por outras ferramentas, como o pudb ou ipdb, definindo a variável de ambiente para o módulo desejado. Por exemplo, PYTHONBREAKPOINT=ipdb.set_trace fará com que breakpoint() use o IPython debugger.

Veja como usar breakpoint() no exemplo anterior:

def calcular_media(numeros):
    total = 0
    for i in range(len(numeros)):
        total += numeros[i]
        breakpoint()  # Pausa a execução aqui
    return total / len(numeros)

notas = [7, 8, 9]
media = calcular_media(notas)
print(f"A média é {media}")

O comportamento é idêntico ao pdb.set_trace(), mas com a vantagem de ser mais limpo e configurável. É recomendável usar breakpoint() em vez de pdb.set_trace() em código novo.

Logging vs print

Muitos programadores iniciantes usam print() para depurar, inserindo prints em vários pontos do código para ver o valor de variáveis. Embora isso funcione em programas pequenos, torna-se problemático em projetos maiores: você precisa remover os prints depois, eles poluem a saída padrão e não oferecem níveis de severidade ou formatação flexível.

A biblioteca padrão logging é a solução profissional. Ela permite que você registre mensagens com diferentes níveis de severidade (DEBUG, INFO, WARNING, ERROR, CRITICAL), direcione as mensagens para diferentes destinos (console, arquivo, etc.) e configure formatação. Além disso, você pode desativar facilmente as mensagens de debug em produção, sem precisar remover o código.

Exemplo básico de logging:

import logging

# Configura o logging para exibir mensagens a partir do nível DEBUG
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')

def calcular_media(numeros):
    total = 0
    for i in range(len(numeros)):
        total += numeros[i]
        logging.debug(f"i={i}, total={total}")
    media = total / len(numeros)
    logging.info(f"Média calculada: {media}")
    return media

notas = [7, 8, 9]
media = calcular_media(notas)
print(f"A média é {media}")

Ao executar, você verá as mensagens de debug e info no console, com timestamp e nível. Em produção, você pode definir o nível para WARNING ou ERROR, e as mensagens de debug não aparecerão. Isso é muito mais limpo e controlável do que usar print.

Comparando: print é útil para scripts rápidos, mas logging é a escolha certa para aplicações reais. Sempre que você sentir a necessidade de adicionar um print para depurar, considere usar logging.debug() em vez disso.

Ferramentas

Além do pdb e breakpoint(), existem outras ferramentas que podem ajudar no debugging de código Python. Aqui estão algumas delas:

  • IPython / ipdb: O IPython oferece um shell interativo aprimorado, e ipdb é o depurador integrado que fornece recursos como autocompletar, destaque de sintaxe e melhor visualização de objetos. Você pode instalá-lo com pip install ipdb e usá-lo como depurador padrão definindo a variável de ambiente.
  • pudb: Um depurador visual em modo texto que oferece uma interface mais amigável, com painéis para código, variáveis e pilha de chamadas. Instale com pip install pudb e use pudb.set_trace().
  • IDE Debuggers: IDEs como PyCharm, VS Code e Eclipse+PyDev têm depuradores gráficos integrados que permitem definir breakpoints, inspecionar variáveis e executar passo a passo com uma interface visual. Esses são extremamente poderosos e muitas vezes preferidos por desenvolvedores profissionais.
  • trace: O módulo trace permite rastrear a execução do programa, mostrando quais linhas são executadas. Pode ser útil para análise de cobertura de código.
  • cProfile: Para problemas de desempenho, o cProfile é um profiler que mostra quanto tempo é gasto em cada função. Embora não seja exatamente um debugger, ajuda a identificar gargalos.

Escolher a ferramenta certa depende do contexto. Para debugging interativo rápido, pdb/breakpoint() são ótimos. Para debugging mais visual, um IDE é melhor. Para análise de desempenho, use um profiler.

Boas práticas e observações finais

Quando você encontrar um bug, tente reproduzi-lo de forma isolada, com um exemplo mínimo. Isso facilita a identificação da causa. Use breakpoints em pontos estratégicos, como antes de uma operação que você suspeita estar causando o problema. Não tenha medo de explorar o estado do programa: variáveis, pilha de chamadas, valores de argumentos.

Além disso, lembre-se de que o debugging não é apenas sobre corrigir o erro imediato, mas também sobre entender o sistema. Depois de corrigir, escreva um teste de regressão para garantir que o bug não volte. E, se possível, reflita sobre como o bug surgiu e se há uma maneira de preveni-lo no futuro.

Por fim, não use print como sua principal ferramenta de debugging. Invista tempo em aprender logging e as ferramentas de depuração da sua IDE. Isso pagará dividendos em produtividade e qualidade de código.

Referências

Exercícios

  1. Escreva um programa que use breakpoint() para inspecionar o valor de uma variável dentro de um loop. Execute e pratique os comandos do pdb (n, s, p, c, q).

    ✓ Resposta: Um exemplo simples:
    for i in range(5):
        breakpoint()
        print(i)
    
    Ao executar, o programa pausa e você pode usar comandos como p i para ver o valor de i, n para avançar, etc.
  2. Converta um programa que usa print para depuração em um que use logging com nível DEBUG. Mostre como configurar o logging para exibir as mensagens no console.

    ✓ Resposta: Antes:
    def soma(a, b):
        print(f"a={a}, b={b}")
        return a + b
    
    Depois:
    import logging
    logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s')
    def soma(a, b):
        logging.debug(f"a={a}, b={b}")
        return a + b
    
  3. Explique a diferença entre pdb.set_trace() e breakpoint() e por que você deve preferir breakpoint() em código novo.

    ✓ Resposta: pdb.set_trace() é uma chamada explícita ao módulo pdb, enquanto breakpoint() é uma função integrada que por padrão chama pdb.set_trace(), mas pode ser configurada para usar outros depuradores ou ser desativada via variável de ambiente PYTHONBREAKPOINT. Isso oferece mais flexibilidade e é mais limpo, por isso é recomendado para código novo.
  4. Dado o código abaixo, use breakpoint() para encontrar o erro (a variável total não está acumulando corretamente).
    def calcular_total(precos):
        total = 0
        for preco in precos:
            total = preco  # Erro: deveria ser total += preco
        return total
    

    ✓ Resposta: Adicione breakpoint() dentro do loop para inspecionar total e preco em cada iteração. Você verá que total é sobrescrito em vez de acumulado. A correção é total += preco.
  5. Pesquise sobre o depurador visual pudb e escreva um pequeno tutorial de como instalá-lo e usá-lo em um programa simples.

    ✓ Resposta: Instale com pip install pudb. No código, importe pudb e chame pudb.set_trace(). Ao executar, você verá uma interface interativa com navegação por teclado. Por exemplo:
    import pudb
    pudb.set_trace()
    print("Olá")
    
    Use as setas para navegar, n para avançar, s para entrar em funções, c para continuar, etc. Consulte a documentação oficial para mais detalhes.