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 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 -
tb_frame
- The frame object corresponding to the function where the
exception was caught.
tb_lineno
- The line number in the source code pointing to the line in
the function where the exception occured.
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.
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.
In this post, I have discussed a special case of chained exceptions and the traceback object as described in the Data model.