Search code examples
pythonpython-3.xipythonpdbipdb

How to debug the stack trace that causes a subsequent exception in python?


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.


Solution

  • 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
    

    EDIT - An alternative inferior approach

    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: