pythonpdbipdb

How can I extract local variables from a stack trace?


Suppose I have a function that raises unexpected exceptions, so I wrap it in ipdb:

def boom(x, y):
    try:
        x / y
    except Exception as e:
        import ipdb; ipdb.set_trace()

def main():
    x = 2
    y = 0
    boom(x, y)

if __name__ == '__main__':
    main()

I can move up the stack to find out what values x and y have:

$ python crash.py 
> /tmp/crash.py(6)boom()
      5     except Exception as e:
----> 6         import ipdb; ipdb.set_trace()
      7 

ipdb> u
> /tmp/crash.py(11)main()
     10     y = 0
---> 11     boom(x, y)
     12 

ipdb> p y
0

However, when debugging, I want to just put a debugger at the top level:

def boom(x, y):
    x / y

def main():
    x = 2
    y = 0
    boom(x, y)

if __name__ == '__main__':
    try:
        main()
    except Exception as e:
        import ipdb; ipdb.set_trace()

I can display the traceback, but I can't view the variables inside the function called:

$ python crash.py 
> /tmp/crash.py(14)<module>()
     12         main()
     13     except Exception as e:
---> 14         import ipdb; ipdb.set_trace()

ipdb> !import traceback; traceback.print_exc(e)
Traceback (most recent call last):
  File "crash.py", line 12, in <module>
    main()
  File "crash.py", line 8, in main
    boom(x, y)
  File "crash.py", line 3, in boom
    x / y
ZeroDivisionError: integer division or modulo by zero
ipdb> d # I want to see what value x and y had!
*** Newest frame

The exception object clearly still has references to the stack when the exception occurred. Can I access x and y here, even though the stack has unwound?


Solution

  • Turns out that it is possible to extract variables from a traceback object.

    To manually extract values:

    ipdb> !import sys
    ipdb> !tb = sys.exc_info()[2]
    ipdb> p tb.tb_next.tb_frame.f_locals
    {'y': 0, 'x': 2}
    

    Even better, you can use an exception to explicitly do post-mortem debugging on that stack:

    import sys
    
    def boom(x, y):
        x / y
    
    def main():
        x = 2
        y = 0
        boom(x, y)
    
    if __name__ == '__main__':
        try:
            main()
        except Exception as e:
            # Most debuggers allow you to just do .post_mortem()
            # but see https://github.com/gotcha/ipdb/pull/94
            tb = sys.exc_info()[2]
            import ipdb; ipdb.post_mortem(tb)
    

    Which gets us straight to the offending code:

    > /tmp/crash.py(4)boom()
          3 def boom(x, y):
    ----> 4     x / y
          5 
    
    ipdb> p x
    2