Cómo leer los tracebacks

Una visión general de cómo leer los tracebacks de Python para depurar.


Objeto de marco

Cuando se llama a una función, se crea un objeto de marco para contener las variables locales y los argumentos que se pasan a la función. Cuando la función devuelve, el objeto de marco se destruye. Cuando se llama a la función B dentro de la función A, los valores de la función B se guardan en un objeto de marco, que después se coloca encima del objeto de marco de la función A en la pila de llamadas.

Pila de llamadas

La pila de llamadas es un conjunto de objetos de marco de las funciones activas en ese momento. Si la función A ha llamado a la función B y la función B ha llamado a la función C, los objetos de marco de las tres funciones estarán en la pila de llamadas. Cuando la función C devuelve, su objeto de marco se saca de la pila y solo quedan en la pila de llamadas los objetos de marco de las funciones A y B.

Traceback

Un traceback es un informe de todos los objetos de marco que hay en la pila en un momento concreto. Cuando un programa de Python se encuentra con una excepción no controlada, imprime el mensaje de la excepción y un traceback. El traceback muestra dónde se lanzó la excepción y qué funciones se llamaron hasta llegar a ese punto.

Cómo leer un traceback

ValueError es una excepción habitual.

El siguiente es un ejemplo de ValueError que se produce al intentar asignar dos variables a la izquierda a partir de un solo valor a la derecha:

>>> 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)

En los tracebacks, la llamada más reciente va en último lugar, así que el punto por el que hay que empezar a leer es la excepción que aparece abajo. Si leemos hacia arriba desde ahí, podemos ver cómo se llegó a esa instrucción. Si colocamos la línea problemática dentro de una función y luego llamamos a la función, vemos un traceback más largo:

>>> 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)

Si vamos hacia atrás desde abajo, vemos que la llamada en la que ocurrió la excepción está en la línea 2, dentro de my_func. Llegamos hasta ahí llamando a my_func en la línea 5.

Excepciones habituales

Python define más de 60 clases de excepciones integradas. Aquí tienes un breve resumen de algunas de las excepciones más habituales y de lo que indican.

SyntaxError

Python lanza un SyntaxError cuando no puede entender el código debido a una sintaxis no válida. Por ejemplo, puede haber un paréntesis de apertura sin su paréntesis de cierre correspondiente.

Haz clic aquí para ver un ejemplo de código.

Si ejecutas 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

obtendrás una traza de pila similar a esta (fíjate en el mensaje de la última línea):

.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

Python lanza un AssertionError cuando falla una instrucción assert (ver más abajo).

Haz clic aquí para ver un ejemplo de código.

Si ejecutas este código:

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


distance("ab", "abc")

obtendrás una traza de pila similar a esta (fíjate en el mensaje de la última línea):

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

Se lanza un AttributeError cuando el código (¡o una prueba unitaria!) intenta acceder al atributo de un objeto, pero ese objeto no tiene tal atributo. Por ejemplo, una prueba unitaria espera que un objeto Robot tenga un atributo direction, pero cuando intenta acceder a robot.direction, este no existe.

También puede indicar una errata, como usar "Hello".lowercase() cuando la sintaxis correcta es "Hello".lower(). "Hello".lowercase() lanza AttributeError: 'str' object has no attribute 'lowercase'.

Haz clic aquí para ver un ejemplo con la clase Robot.

Si ejecutas 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

obtendrás una traza de pila similar a esta (fíjate en el mensaje de la última línea):

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'
Haz clic aquí para ver un ejemplo con un string.

Si ejecutas este código:

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


distance("ab", "abc")

obtendrás una traza de pila similar a esta (fíjate en el mensaje de la última línea):

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

ImportError

Se lanza un ImportError cuando el código intenta importar algo, pero Python no puede hacerlo. Por ejemplo, una prueba unitaria de Guidos Gorgeous Lasagna hace from lasagna import bake_time_remaining, pero puede que el archivo de solución lasgana.py no defina bake_time_remaining.

Haz clic aquí para ver un ejemplo de código

Si ejecutas el archivo lasgana.py sin la función definida, obtendrás el siguiente error:

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.

Si ejecutas ese código, obtendrás un error similar a este. (Fíjate en la última línea.)

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

De forma similar a IndexError, esta excepción se lanza cuando se usa una clave para buscar un valor en un diccionario, pero la clave no está definida en el diccionario.

Haz clic aquí para ver un ejemplo de código

Fíjate en el siguiente código.

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.

Si ejecutas ese código, obtendrás un error similar a este. (Fíjate en la última línea.)

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, se lanza un TypeError cuando a una función se le pasa un tipo de dato incorrecto o cuando se usa un tipo de dato incorrecto en una operación.

Haz clic aquí para ver un ejemplo de código

Fíjate en el siguiente código.

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


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

Si ejecutas ese código, obtendrás un error similar a este. (Fíjate en la última línea.)

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

Un ValueError se suele lanzar cuando se pasa un valor no válido a una función.

Haz clic aquí para ver un ejemplo de código

Fíjate en que las raíces cuadradas reales solo existen para números positivos. Llamar a math.sqrt(-1) lanzará ValueError: math domain error, ya que -1 no es un valor válido para una raíz cuadrada. En términos técnicos (matemáticos), -1 no pertenece al dominio de las raíces cuadradas.

import math

math.sqrt(-1)

Si ejecutas ese código, obtendrás un error similar a este. (Fíjate en la última línea.)

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

Cómo usar la función print

A veces no se lanza ningún error, pero un valor no es el esperado. Esto puede resultar especialmente desconcertante si el valor es el resultado de una cadena de cálculos. En una situación así, puede ser útil observar el valor en cada paso para ver cuál es el que no se comporta como se espera. La función print se puede usar para imprimir el valor en la consola. El siguiente es un ejemplo de una función que no devuelve el 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

Cuando se pasa 5 a la función, el valor esperado es 8, pero devuelve 10.0. Para solucionarlo, el cálculo se desglosa de modo que el valor pueda inspeccionarse en cada paso.

# 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))

El nivel se configura como INFO porque el nivel predeterminado es WARNING. Para tener un registro persistente, se puede configurar el registrador para que escriba en un archivo, así:

>>> 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 es una instrucción que siempre debe evaluarse como True, a menos que haya un bug en el programa. Cuando un assert se evalúa como False, lanza un AssertionError. El traceback del AssertionError puede incluir un mensaje opcional que forma parte de la instrucción assert. Aunque el mensaje es opcional, es una buena práctica incluir siempre uno en la definición del assert.

El siguiente es un ejemplo del uso de 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

Si empezamos a leer el traceback por abajo (como deberíamos), vemos enseguida que el problema es que 0 no debería pasarse como divisor.

assert también se puede usar para comprobar que un valor es del 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

Una vez identificado el bug, plantéate sustituir el assert por una gestión de errores. Esto se debe a que todas las instrucciones assert se pueden desactivar si ejecutas Python con las opciones -O o -OO, o si estableces la variable de entorno PYTHONOPTIMIZE en 1 o 2. Establecer PYTHONOPTIMIZE en 1 equivale a ejecutar Python con la opción -O, que desactiva las aserciones. Establecer PYTHONOPTIMIZE en 2 equivale a ejecutar Python con la opción -OO, que desactiva las aserciones y además elimina las docstrings del bytecode. Reducir el bytecode es una forma de hacer que el código se ejecute más rápido.

Depurador de Python

Python incluye un depurador integrado, pdb. Se puede usar para recorrer el código paso a paso e inspeccionar variables. También puedes establecer puntos de interrupción con él. Para empezar, primero tienes que hacer import pdb y luego llamar a pdb.set_trace() donde quieras empezar a depurar:

import pdb

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

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

Si ejecutas este código, aparecerá un prompt de pdb donde puedes escribir comandos. Escribe help para obtener una lista de comandos. Los más habituales son step, que entra en la función llamada en esa línea. next pasa por encima de una llamada a función y avanza a la siguiente línea. where te indica en qué línea estás. Algunos otros comandos útiles son whatis <variable>, que te indica el tipo de una variable, y print(<variable>), que imprime el valor de una variable. También puedes usar simplemente <variable> para imprimir el valor de una variable. Otro comando es jump <line number>, que salta a un número de línea concreto.

Aquí tienes un pequeño ejemplo de cómo usar el depurador basado en el código anterior. Fíjate en que, tanto en este ejemplo como en los siguientes, en las plataformas MacOS o Linux las rutas de archivo usarían barras inclinadas:

>>> 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)

Los puntos de interrupción se establecen con break <filename>:<line number> <condition>, donde la condición es una condición opcional que debe cumplirse para que se active el punto de interrupción. Puedes escribir simplemente break para obtener una lista de los puntos de interrupción que has establecido. Para desactivar un punto de interrupción, puedes escribir disable <breakpoint number>. Para activar un punto de interrupción, puedes escribir enable <breakpoint number>. Para eliminar un punto de interrupción, puedes escribir clear <breakpoint number>. Para continuar la ejecución, puedes escribir continue o c. Para salir del depurador, puedes escribir quit o q.

Aquí tienes un ejemplo de cómo usar los comandos del depurador anteriores, partiendo del código de antes:

>>> 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
...

En Python 3.7 y versiones posteriores hay una forma más sencilla de crear puntos de interrupción. Basta con escribir breakpoint() allí donde lo necesites para crear uno.

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