Python (and ipython) has very powerful post-mortem debugging capabilities, allowing variable inspection and command execution at each scope in the traceback. The up/down debugger commands allow changing frame for the stack trace of the final exception, but what about the __cause__
of that exception, as defined by the raise ... from ...
syntax?
Python 3.7.6 (default, Jan 8 2020, 13:42:34)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.11.1 -- An enhanced Interactive Python. Type '?' for help.
In [1]: def foo():
...: bab = 42
...: raise TypeError
...:
In [2]: try:
...: foo()
...: except TypeError as err:
...: barz = 5
...: raise ValueError from err
...:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-dd046d7cece0> in <module>
1 try:
----> 2 foo()
3 except TypeError as err:
<ipython-input-1-da9a05838c59> in foo()
2 bab = 42
----> 3 raise TypeError
4
TypeError:
The above exception was the direct cause of the following exception:
ValueError Traceback (most recent call last)
<ipython-input-2-dd046d7cece0> in <module>
3 except TypeError as err:
4 barz = 5
----> 5 raise ValueError from err
6
ValueError:
In [3]: %debug
> <ipython-input-2-dd046d7cece0>(5)<module>()
2 foo()
3 except TypeError as err:
4 barz = 5
----> 5 raise ValueError from err
6
ipdb> barz
5
ipdb> bab
*** NameError: name 'bab' is not defined
ipdb> down
*** Newest frame
ipdb> up
*** Oldest frame
Is there a way to access bab
from the debugger?
EDIT: I realized post-mortem debugging isn't just a feature of ipython and ipdb, it's actually part of vanilla pdb. The above can also be reproduced by putting the code into a script testerr.py
and running python -m pdb testerr.py
and running continue
. After the error, it says
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
and gives a debugger at the same spot.
You can use the with_traceback(tb)
method to preserve the original exception's traceback:
try:
foo()
except TypeError as err:
barz = 5
raise ValueError().with_traceback(err.__traceback__) from err
Note that I have updated the code to raise an exception instance rather than the exception class.
Here is the full code snippet in iPython:
In [1]: def foo():
...: bab = 42
...: raise TypeError()
...:
In [2]: try:
...: foo()
...: except TypeError as err:
...: barz = 5
...: raise ValueError().with_traceback(err.__traceback__) from err
...:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-a5a6d81e4c1a> in <module>
1 try:
----> 2 foo()
3 except TypeError as err:
<ipython-input-1-ca1efd1bee60> in foo()
2 bab = 42
----> 3 raise TypeError()
4
TypeError:
The above exception was the direct cause of the following exception:
ValueError Traceback (most recent call last)
<ipython-input-2-a5a6d81e4c1a> in <module>
3 except TypeError as err:
4 barz = 5
----> 5 raise ValueError().with_traceback(err.__traceback__) from err
6
<ipython-input-2-a5a6d81e4c1a> in <module>
1 try:
----> 2 foo()
3 except TypeError as err:
4 barz = 5
5 raise ValueError().with_traceback(err.__traceback__) from err
<ipython-input-1-ca1efd1bee60> in foo()
1 def foo():
2 bab = 42
----> 3 raise TypeError()
4
ValueError:
In [3]: %debug
> <ipython-input-1-ca1efd1bee60>(3)foo()
1 def foo():
2 bab = 42
----> 3 raise TypeError()
4
ipdb> bab
42
ipdb> u
> <ipython-input-2-a5a6d81e4c1a>(2)<module>()
1 try:
----> 2 foo()
3 except TypeError as err:
4 barz = 5
5 raise ValueError().with_traceback(err.__traceback__) from err
ipdb> u
> <ipython-input-2-a5a6d81e4c1a>(5)<module>()
2 foo()
3 except TypeError as err:
4 barz = 5
----> 5 raise ValueError().with_traceback(err.__traceback__) from err
6
ipdb> barz
5
Addressing @user2357112supportsMonica's first comment, if you wish to avoid multiple dumps of the original exception's traceback in the log, it's possible to raise from None
. However, as @user2357112supportsMonica's second comment states, this hides the original exception's message. This is particularly problematic in the common case where you're not post-mortem debugging but rather inspecting a printed traceback.
try:
foo()
except TypeError as err:
barz = 5
raise ValueError().with_traceback(err.__traceback__) from None
Here is the code snippet in iPython:
In [4]: try:
...: foo()
...: except TypeError as err:
...: barz = 5
...: raise ValueError().with_traceback(err.__traceback__) from None
...:
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-6-b090fb9c510e> in <module>
3 except TypeError as err:
4 barz = 5
----> 5 raise ValueError().with_traceback(err.__traceback__) from None
6
<ipython-input-6-b090fb9c510e> in <module>
1 try:
----> 2 foo()
3 except TypeError as err:
4 barz = 5
5 raise ValueError().with_traceback(err.__traceback__) from None
<ipython-input-2-ca1efd1bee60> in foo()
1 def foo():
2 bab = 42
----> 3 raise TypeError()
4
ValueError:
In [5]: %debug
> <ipython-input-2-ca1efd1bee60>(3)foo()
1 def foo():
2 bab = 42
----> 3 raise TypeError()
4
ipdb> bab
42
ipdb> u
> <ipython-input-6-b090fb9c510e>(2)<module>()
1 try:
----> 2 foo()
3 except TypeError as err:
4 barz = 5
5 raise ValueError().with_traceback(err.__traceback__) from None
ipdb> u
> <ipython-input-6-b090fb9c510e>(5)<module>()
3 except TypeError as err:
4 barz = 5
----> 5 raise ValueError().with_traceback(err.__traceback__) from None
6
ipdb> barz
5
Raising from None
is required since otherwise the chaining would be done implicitly, attaching the original exception as the new exception’s __context__
attribute. Note that this differs from the __cause__
attribute which is set when the chaining is done explicitly.
In [6]: try:
...: foo()
...: except TypeError as err:
...: barz = 5
...: raise ValueError().with_traceback(err.__traceback__)
...:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-5-ee78991171cb> in <module>
1 try:
----> 2 foo()
3 except TypeError as err:
<ipython-input-2-ca1efd1bee60> in foo()
2 bab = 42
----> 3 raise TypeError()
4
TypeError:
During handling of the above exception, another exception occurred:
ValueError Traceback (most recent call last)
<ipython-input-5-ee78991171cb> in <module>
3 except TypeError as err:
4 barz = 5
----> 5 raise ValueError().with_traceback(err.__traceback__)
6
<ipython-input-5-ee78991171cb> in <module>
1 try:
----> 2 foo()
3 except TypeError as err:
4 barz = 5
5 raise ValueError().with_traceback(err.__traceback__)
<ipython-input-2-ca1efd1bee60> in foo()
1 def foo():
2 bab = 42
----> 3 raise TypeError()
4
ValueError: