Back to index

More on the Python traceback

This post adds to the content of the fine article by Real Python titled Understanding the Python traceback. It expands on the topic of chained exceptions and explains what a traceback object is.

First, to repeat for convenience what has been said in that article: when an exception occurs while handling another, we get a chained exception listing in the traceback.

For example, consider the following function that has an error in it:

def get_int(x):
    try:
        x = int(x)
    except ValueError as e:
        print(y)
    return y

When we call this function with invalid data, we get the following traceback:

>>> get_int('a')
Traceback (most recent call last):
  File "<stdin>", line 3, in get_int
ValueError: invalid literal for int() with base 10: 'a'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in get_int
NameError: name 'y' is not defined
>>> 

If we wrap the function call in a try-except block, we get the most recent exception object. If we want the previous (chained) exception, we can get it via the __context__ attribute, as shown below:

>>> try:
...     get_int('a')
... except Exception as e:
...     print(e)
...     print(e.__context__)
... 
name 'y' is not defined
invalid literal for int() with base 10: 'a'
>>> 

Chaining of exceptions also occurs when another exception is explicitly raised when handling the first. The following code shows that the message that appears between exceptions in the traceback is different in this case:

>>> def get_int2(x):
...     try:
...         x = int(x)
...     except ValueError as e:
...         raise RuntimeError('A conversion error occured.') from e
...     return x
... 
>>> get_int2('a')
Traceback (most recent call last):
  File "<stdin>", line 3, in get_int2
ValueError: invalid literal for int() with base 10: 'a'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in get_int2
RuntimeError: A conversion error occured.
>>> 

The message 'The above exception was the direct cause...' makes it clear that a RuntimeError was raised because of the previous ValueError, and not for any other reason. If the function call in this case is wrapped in try-except, then the __cause__ attribute of the exception object gives the cause of the exception - the previous exception.

>>> try:
...     get_int2('a')
... except Exception as e:
...     print(e)
...     print(e.__context__)
...     print(e.__cause__)
... 
A conversion error occured.
invalid literal for int() with base 10: 'a'
invalid literal for int() with base 10: 'a'
>>> 

We see that both __context__ and __cause__ are set to the previous exception. We can follow the chain of exceptions to get the related exceptions with the help of the __cause__ attribute.

If we explicitly raise an exception with from None, then chaining does not occur.

>>> def get_int3(x):
...     try:
...         x = int(x)
...     except ValueError:
...         raise RuntimeError('A conversion error occured.') from None
...     return x
... 
>>> get_int3('a')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in get_int3
RuntimeError: A conversion error occured.
>>> 

Here __cause__ attribute is set to None, which sets another attribute called __suppress_context__ to true. So, even though __context__ will be set to the previous exception, the exception will not appear in the traceback.

The traceback object

The traceback object is described in the Data model. It states:

A traceback object is implicitly created when an exception occurs, and may also be explicitly created by calling types.TracebackType.

When an exception propagates and is finally caught in a function (or in the global scope), the exception object has an attribute __traceback__ that is the traceback object. It, in turn, has the following attributes -

  1. tb_frame - The frame object corresponding to the function where the exception was caught.

  2. tb_lineno - The line number in the source code pointing to the line in the function where the exception occured.

  3. tb_lasti - The exact instruction where the error occured. This can be seen in a disassembly of the function (using the dis module) that gives the bytecode operations.

  4. tb_next - This gives the next traceback object towards the source of the exception. A traceback object is added to the front of the traceback for each level (or frame) encountered by the exception.

Conclusion

In this post, I have discussed a special case of chained exceptions and the traceback object as described in the Data model.

References

  1. Python cookbook by David Beazley and Brian K. Jones

Back to index