Como ler tracebacks

Uma visão geral de como ler tracebacks do Python para depuração.


Objeto de frame

Quando uma função é chamada, um objeto de frame é criado para guardar as variáveis locais e os argumentos passados para a função. Quando a função retorna, o objeto de frame é destruído. Quando a função B é chamada dentro da função A, os valores da função B são colocados em um objeto de frame, que por sua vez é colocado no topo do objeto de frame da função A na pilha de chamadas.

Pilha de chamadas

A pilha de chamadas é uma coleção de objetos de frame das funções ativas no momento. Se a função A chamou a função B e a função B chamou a função C, os objetos de frame das três funções estarão na pilha de chamadas. Assim que a função C retorna, o objeto de frame dela é removido da pilha e apenas os objetos de frame das funções A e B permanecem na pilha de chamadas.

Traceback

Um Traceback é um relatório de todos os objetos de frame na pilha em um determinado momento. Quando um programa Python encontra uma exceção não tratada, imprime a mensagem da exceção e um Traceback. O Traceback mostra onde a exceção foi lançada e quais funções foram chamadas até chegar ali.

Como ler um Traceback

ValueError é uma exceção comum.

O exemplo a seguir mostra um ValueError gerado ao tentar atribuir duas variáveis à esquerda a partir de um único valor à direita:

>>> first, second = [1]
Traceback (most recent call last):
File <stdin>, line 1, in <module>
    first, second = [1]
ValueError: not enough values to unpack (expected 2, got 1)

Os tracebacks são organizados com a chamada mais recente por último, então o lugar para começar a ler o Traceback é a exceção que aparece na parte de baixo. Lendo de baixo para cima a partir dali, dá para ver como o código chegou àquela instrução. Se colocarmos a linha problemática dentro de uma função e depois chamarmos a função, vemos um trace mais longo:

>>> def my_func():
...     first, second = [1]
...
>>> my_func()
Traceback (most recent call last):
  File <stdin>, line 5, in <module>
    my_func()
  File <stdin>, line 2, in my_func
    first, second = [1]
ValueError: not enough values to unpack (expected 2, got 1)

Trabalhando de baixo para cima, vemos que a chamada onde a exceção aconteceu está na linha 2, dentro de my_func. Chegamos ali ao chamar my_func na linha 5.

Exceções comuns

O Python define mais de 60 classes de exceção embutidas. Aqui está uma visão geral breve de algumas das exceções mais comuns e do que elas indicam.

SyntaxError

O Python lança um SyntaxError quando não consegue entender o código por causa de uma sintaxe inválida. Por exemplo, pode haver um parêntese de abertura sem o parêntese de fechamento correspondente.

Clique aqui para ver um exemplo de código.

Executar este código:

def distance(strand_a, strand_b):
    if len(strand_a) != len(strand_b):
        raise ValueError("Strands must be of equal length."  # This is missing the closing parenthesis

vai gerar um stack trace parecido com este (repare na mensagem da última linha):

.usr.local.lib.python3.10.site-packages._pytest.python.py:608: in _importtestmodule
    mod = import_path(self.path, mode=importmode, root=self.config.rootpath)
.usr.local.lib.python3.10.site-packages._pytest.pathlib.py:533: in import_path
    importlib.import_module(module_name)
.usr.local.lib.python3.10.importlib.__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
<frozen importlib._bootstrap>:1050: in _gcd_import ???
<frozen importlib._bootstrap>:1027: in _find_and_load ???
<frozen importlib._bootstrap>:1006: in _find_and_load_unlocked ???
<frozen importlib._bootstrap>:688: in _load_unlocked ???
.usr.local.lib.python3.10.site-packages._pytest.assertion.rewrite.py:168: in exec_module
    exec(co, module.__dict__)
.mnt.exercism-iteration.hamming_test.py:3: in <module>
    from hamming import (  
E     File ".mnt.exercism-iteration.hamming.py", line 10
E       raise ValueError("Strands must be of equal length."
E                       ^
E   SyntaxError: '(' was never closed

AssertionError

O Python lança um AssertionError quando uma instrução assert (veja abaixo) falha.

Clique aqui para ver um exemplo de código.

Executar este código:

def distance(strand_a, strand_b):
    assert len(strand_a) == len(strand_b)


distance("ab", "abc")

vai gerar um stack trace parecido com este (repare na mensagem da última linha):

hamming_test.py:3: in <module>
    from hamming import (
hamming.py:5: in <module>
    distance("ab", "abc")
hamming.py:2: in distance
    assert len(strand_a) == len(strand_b)
E   AssertionError

AttributeError

Um AttributeError é lançado quando o código (ou um teste unitário!) tenta acessar um atributo de um objeto, mas esse objeto não tem esse atributo. Por exemplo, um teste unitário espera que um objeto Robot tenha um atributo direction, mas quando tenta acessar robot.direction, esse atributo não existe.

Isso também pode indicar um erro de digitação, como usar "Hello".lowercase() quando a sintaxe correta é "Hello".lower(). "Hello".lowercase() lança AttributeError: 'str' object has no attribute 'lowercase'.

Clique aqui para ver um exemplo com a classe Robot.

Executar este código:

class Robot:
    def __init__():
        #note that there is no self.direction listed here
        self.position = (0, 0)
        self.orientation = 'SW'

    def forward():
        pass
        

robby = Robot
robby.direction

vai gerar um stack trace parecido com este (repare na mensagem da última linha):

robot_simulator_test.py:3: in <module>
    from robot_simulator import (
robot_simulator.py:12: in <module>
    robby.direction
E   AttributeError: type object 'Robot' has no attribute 'direction'
Clique aqui para ver um exemplo com string.

Executar este código:

def distance(strand_a, strand_b):
    if strand_a.lowercase() == strand_b:
        return 0


distance("ab", "abc")

vai gerar um stack trace parecido com este (repare na mensagem da última linha):

    def distance(strand_a, strand_b):
>       if strand_a.lowercase() == strand_b:
E       AttributeError: 'str' object has no attribute 'lowercase'

ImportError

Um ImportError é lançado quando o código tenta importar algo, mas o Python não consegue. Por exemplo, um teste unitário de Guidos Gorgeous Lasagna faz from lasagna import bake_time_remaining, mas o arquivo de solução lasgana.py pode não definir bake_time_remaining.

Clique aqui para ver um exemplo de código

Executar o arquivo lasgana.py sem a função definida resultaria no erro a seguir:

We received the following error when we ran your code:

  ImportError while importing test module '.mnt.exercism-iteration.lasagna_test.py'.
Hint: make sure your test modules.packages have valid Python names.

Traceback:
.mnt.exercism-iteration.lasagna_test.py:6: in <module>
    from lasagna import (EXPECTED_BAKE_TIME,
E   ImportError: cannot import name 'bake_time_remaining' from 'lasagna' (.mnt.exercism-iteration.lasagna.py)

During handling of the above exception, another exception occurred:
.usr.local.lib.python3.10.importlib.__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
.mnt.exercism-iteration.lasagna_test.py:23: in <module>
    raise ImportError("In your 'lasagna.py' file, we can not find or import the"

E   ImportError: In your 'lasagna.py' file, we can not find or import the function named 'bake_time_remaining()'. Did you mis-name or forget to define it?

### **IndexError**

Python raises an `IndexError` when an invalid index is used to look up a value in a sequence.
This often indicates the index is not computed properly and is often an off-by-one error.


<details>
<summary>Click here for code example</summary>


Consider the following code.

```python
def distance(strand_a, strand_b):
    same = 0
    for i in range(len(strand_a)):
        if strand_a[i] == strand_b[i]:
            same += 1
    return same


distance("abc", "ab")  # Note the first strand is longer than the second strand.

Executar esse código gera um erro parecido com este. (Repare na última linha.)

hamming_test.py:3: in <module>
    from hamming import (
hamming.py:9: in <module>
    distance("abc", "ab")  # Note the first strand is longer than the second strand.
hamming.py:4: in distance
    if strand_a[i] == strand_b[i]:
E   IndexError: string index out of range

KeyError

Parecido com o IndexError, essa exceção é lançada quando se usa uma chave para buscar um valor em um dicionário, mas a chave não está definida no dicionário.

Clique aqui para ver um exemplo de código

Considere o código a seguir.

def to_rna(dna_letter):
    translation = {"G": "C", "C": "G", "A": "U", "T": "A"}
    return translation[dna_letter]


print(to_rna("Q"))  # Note, "Q" is not in the translation.

Executar esse código gera um erro parecido com este. (Repare na última linha.)

rna_transcription_test.py:3: in <module>
    from rna_transcription import to_rna
rna_transcription.py:6: in <module>
    print(to_rna("Q"))
rna_transcription.py:3: in to_rna
    return translation[dna_letter]
E   KeyError: 'Q'

TypeError

Normalmente, um TypeError é lançado quando o tipo errado de dado é passado para uma função ou usado em uma operação.

Clique aqui para ver um exemplo de código

Considere o código a seguir.

def hello(name):  # This function expects a string.
    return 'Hello, ' + name + '!'


print(hello(100))  # 100 is not a string.

Executar esse código gera um erro parecido com este. (Repare na última linha.)

hello_world_test.py:3: in <module>
    import hello_world
hello_world.py:5: in <module>
    print(hello(100))
hello_world.py:2: in hello
    return 'Hello, ' + name + '!'
E   TypeError: can only concatenate str (not "int") to str

ValueError

Um ValueError geralmente é lançado quando um valor inválido é passado para uma função.

Clique aqui para ver um exemplo de código

Repare que raízes quadradas reais só existem para números positivos. Chamar math.sqrt(-1) lança ValueError: math domain error, já que -1 não é um valor válido para uma raiz quadrada. Em termos técnicos (matemáticos), -1 não pertence ao domínio das raízes quadradas.

import math

math.sqrt(-1)

Executar esse código gera um erro parecido com este. (Repare na última linha.)

square_root_test.py:3: in <module>
    from square_root import (
square_root.py:3: in <module>
    math.sqrt(-1)
E   ValueError: math domain error

Usando a função print

Às vezes nenhum erro é lançado, mas um valor não é o esperado. Isso pode ser especialmente confuso quando o valor é o resultado de uma sequência de cálculos. Numa situação dessas, pode ser útil olhar o valor em cada etapa para descobrir qual passo não está se comportando como o esperado. A função print pode ser usada para imprimir o valor no console. Veja a seguir um exemplo de função que não retorna o valor esperado:

# the intent is to pass an integer to this function and get an integer back
def halve_and_quadruple(num):
    return (num / 2) * 4

Quando passamos 5 para a função, o valor esperado é 8, mas ela retorna 10.0. Para investigar, o cálculo é dividido em partes para que o valor possa ser inspecionado em cada etapa.

# the intent is to pass an integer to this function and get an integer back
def halve_and_quadruple(num):
    # verify the number in is what is expected
    # prints 5
    print(num)
    # we want the int divided by an integer to be an integer
    # but this prints 2.5! We've found our mistake.
    print(num / 2)
    # this makes sense, since 2.5 x 4 = 10.0
    print((num / 2) * 4)
    return (num / 2) * 4

What the `print` calls revealed is that we used `/` when we should have used `//`, the [floor division operator][floor division operator].

## Logging

[Logging][logging] can be used similarly to `print`, but it is more powerful.
What is logged can be configured by the logging severity (e.g., 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'.)
A call to the `logging.error` function can pass `True` to the `exc_info` parameter, which will additionally log the stack trace.
By configuring multiple handlers, logging can write to more than one place with the same logging function.

Following is an example of logging printed to the console:

```python

>>> import logging
>>>
>>> # configures minimum logging level as INFO
>>> logging.basicConfig(level=logging.INFO)
>>>
>>> def halve_and_quadruple(num):
...     # prints INFO:root: num == 5
...     logging.info(f" num == {num}")
...     return (num // 2) * 4
...
>>> print(halve_and_quadruple(5))

O nível é configurado como INFO porque o nível padrão é WARNING. Para um log persistente, dá para configurar o logger para escrever em um arquivo, assim:

>>> import logging
...
>>> # configures the output file name to example.log, and the minimum logging level as INFO
>>> logging.basicConfig(filename='example.log', level=logging.INFO)
...
... def halve_and_quadruple(num):
...     # prints INFO:root: num == 5 to the example.log file
...     logging.info(f" num == {num}")
...     return (num // 2) * 4
...
>>> print(halve_and_quadruple(5))

assert

assert é uma instrução que deve sempre avaliar para True, a menos que haja um bug no programa. Quando um assert avalia para False, ele lança um AssertionError. O Traceback do AssertionError pode incluir uma mensagem opcional que faz parte da instrução assert. Apesar de a mensagem ser opcional, é uma boa prática sempre incluir uma na definição do assert.

Veja a seguir um exemplo de uso do assert:

>>> def int_division(dividend, divisor):
...     assert divisor != 0, "divisor must not be 0"
...     return dividend // divisor
...
>>> print(int_division(2, 1))
2
>>> print(int_division(2, 0))
Traceback (most recent call last):
  File <stdin>, line 7, in <module>
    print(int_division(2, 0))
          ^^^^^^^^^^^^^^^^^^
  File <stdin>, line 2, in int_division
    assert divisor != 0, "divisor must not be 0"
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: divisor must not be 0

Se começarmos a ler o Traceback pela parte de baixo (como deveríamos), vemos logo que o problema é que 0 não deveria ser passado como divisor.

O assert também pode ser usado para testar se um valor é do tipo esperado:

>>> import numbers
...
...
... def int_division(dividend, divisor):
...     assert divisor != 0, "divisor must not be 0"
...     assert isinstance(divisor, numbers.Number), "divisor must be a number"
...     return dividend // divisor
...
>>> print(int_division(2, 1))
2
>>> print(int_division(2, '0'))
Traceback (most recent call last):
  File <stdin>, line 11, in <module>
    print(int_division(2, '0'))
          ^^^^^^^^^^^^^^^^^^^^
  File <stdin>, line 6, in int_division
    assert isinstance(divisor, numbers.Number), "divisor must be a number"
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: divisor must be a number

Depois de identificar um bug, considere substituir o assert por um tratamento de erros. Isso porque todas as instruções assert podem ser desativadas executando o Python com as opções -O ou -OO, ou definindo a variável de ambiente PYTHONOPTIMIZE como 1 ou 2. Definir PYTHONOPTIMIZE como 1 é equivalente a executar o Python com a opção -O, que desativa as asserções. Definir PYTHONOPTIMIZE como 2 é equivalente a executar o Python com a opção -OO, que desativa as asserções e ainda remove as docstrings do bytecode. Reduzir o bytecode é uma forma de fazer o código rodar mais rápido.

Depurador do Python

O Python tem um depurador embutido, o pdb. Ele pode ser usado para percorrer o código passo a passo e inspecionar variáveis. Você também pode definir breakpoints com ele. Para começar, primeiro faça import pdb e depois chame pdb.set_trace() onde você quer iniciar a depuração:

import pdb

def add(num1, num2):
    return num1 + num2

pdb.set_trace()
sum = add(1,5)
print(sum)

Executar esse código abre um prompt do pdb, onde você pode digitar comandos. Digite help para ver uma lista de comandos. Os mais comuns são step, que entra na função chamada naquela linha. next passa por cima de uma chamada de função e vai para a próxima linha. where mostra em qual linha você está. Outros comandos úteis são whatis <variable>, que mostra o tipo de uma variável, e print(<variable>), que imprime o valor de uma variável. Você também pode simplesmente usar <variable> para imprimir o valor de uma variável. Outro comando é jump <line number>, que salta para um número de linha específico.

Veja um pequeno exemplo de como usar o depurador, com base no código anterior. Repare que, para este e os próximos exemplos, plataformas MacOS ou Linux teriam caminhos de arquivo com barras (/):

>>> python pdb.py
... > c:\pdb.py(7)<module>()
... -> sum = add(1,5)
... (Pdb)
>>> step
... > c:\pdb.py(3)add()
... -> def add(num1, num2):
... (Pdb)
>>> whatis num1
... <class 'int'>
>>> print(num2)
... 5
>>> next
... > c:\pdb.py(4)add()
... -> return num1 + num2
... (Pdb)
>>> jump 3
... > c:\pdb.py(3)add()
... -> def add(num1, num2):
... (Pdb)

Breakpoints são definidos com break <filename>:<line number> <condition>, em que a condição é uma condição opcional que precisa ser verdadeira para o breakpoint ser acionado. Você pode simplesmente digitar break para obter uma lista dos breakpoints que definiu. Para desativar um breakpoint, digite disable <breakpoint number>. Para ativar um breakpoint, digite enable <breakpoint number>. Para excluir um breakpoint, digite clear <breakpoint number>. Para continuar a execução, digite continue ou c. Para sair do depurador, digite quit ou q.

Veja um exemplo de como usar os comandos do depurador acima, com base no código anterior:

>>> python pdb.py
... > c:\pdb.py(7)<module>()
... -> sum = add(1,5)
... (Pdb)
>>> break
...
>>> break pdb:4
... Breakpoint 1 at c:\pdb.py:4
>>> break
... Num Type         Disp Enb   Where
... 2   breakpoint   keep yes   at c:\pdb.py:4
>>> c # continue
... > c:\pdn.py(4)add()
... -> return num1 + num2
>>> disable break 1
... Disabled breakpoint 1 at c:\pdb.py:4
>>> break
... Num Type         Disp Enb   Where
... 1   breakpoint   keep no    at c:\pdb.py:4
...         breakpoint already hit 1 time
>>> clear break 1
... Deleted breakpoint 1 at c:\pdb.py:4
>>> break
...

No Python 3.7+ existe uma forma mais fácil de criar breakpoints. Basta escrever breakpoint() onde for preciso para criar um.

def add(num1, num2):
    breakpoint()
    return num1 + num2

breakpoint()
sum = add(1,5)
print(sum)
>>> python pdb.py
... > c:\pdb.py(7)<module>()
... -> sum = add(1,5)
... (Pdb)
>>> c # continue
... > c:\pdb.py(5)add()
... -> return num1 + num2