This is an explanatory article related to my talk at MUPy ― Become a pdb power-user .
What’s pdb?pdb is a module from python’s standard library that allows us to do things like:
Stepping through source code Setting conditional breakpoints Inspecting stack trace Viewing source code Running Python code in a context Post-mortem debugging Why pdb?It is not necessary to use pdb all the time, sometimes we can get away with simple print statements or logging.
But these other approaches are most of the time not good enough and don’t give us enough control while debugging. Plus after debugging we also have to take care of removing the print statements that we had added to our program just for debugging purpose, this isn’t true for logging though as we can filter out logs easily. But at the end both of these approaches clutter our code and don’t give us enough debugging power either.
How to startpdb?There are multiple ways to start pdb depending on your use case.
1. Starting program under debugger controlWe can start a script itself under debugger’s control by executing the script using -m pdb argument. Let’s run script.py :
$ python -m pdb script.py > /pdb-mupy/script.py(1)<module>() -> """I am the first script in this demo""" (Pdb)In this mode Python will stop the program on first line and you are going to be inside debugger( (Pdb) is pdb’s prompt). At this point you can either set breakpoints or continue executing your program.
Another special thing about it is that after the program completion if no exception occurred then your program will restart in same mode otherwise it will start in post-mortem mode. After post-mortem mode you can restart the program again.
2. Running code under debugger controlInstead of running the whole code under debugger control we can run particular code under using pdb.run , pdb.runeval and pdb.runcall .
>>> import pdb >>> import script >>> pdb.run('script.divide(10, 5)') > <string>(1)<module>()->None (Pdb) s # we can run any pdb command here --Call-- > /pdb-mupy/script.py(6)divide() -> def divide(numerator, denominator): (Pdb) n > /pdb-mupy/script.py(7)divide() -> return numerator / denominator (Pdb) p numerator, denominator (10, 5) (Pdb) c >>>Here <string>(1)<module>() means that we are at the start of string passed to run() and no code has executed yet. In the above example we stepped into the divide function using s (don’t worry about s , n , c etc, we will be covering them in detail).
runeval() does the same thing as run() except that it also returns the value of executed code.
runcall() allows us to pass a Python callable itself instead of a string.
>>> pdb.runcall(script.divide, 10, 5) > /pdb-mupy/script.py(7)divide() -> return numerator / denominator (Pdb) 3. Set a hardcoded breakpointThis is the most common way to debug programs, it basically involves adding the line pdb.set_trace() in the source code wherever we want our program to stop.
4. Post-mortem debuggingPost-mortem debugging allows us to debug a dead program using its traceback object. In post-mortem debugging we can inspect the state of the program at the time it died. But apart from inspecting the state we can’t do much here(like stepping through the code) because like the name suggests we are performing post-mortem of a dead program.
By default the -m pdb we had discussed earlier puts us in post-mortem mode if an exception occurs. Other ways are using: pdb.pm() and pdm.post_mortem() .
pdb.pm() will take us to the post-mortem mode for the exception found in sys.last_traceback .
On the other hand pdb.post_mortem() excepts an optional traceback object otherwise will try to handle the exception currently being handled.
>>> import pdb >>> import script >>> script.divide(10, 0) Traceback (most recent call last): File "<ipython-input-8-fe270324adad>", line 1, in <module> script.divide(10, 0) File "script.py", line 7, in divide return numerator / denominator ZeroDivisionError: integer division or modulo by zeroNow to inspect the state at the time this above exception occurred usign pdb.pm() .
>>> pdb.pm() > /Users/ashwini/work/instamojo/pdb-mupy/script.py(7)divide() -> return numerator / denominator (Pdb) args # Arguments passed to the function at that time numerator = 10 denominator = 0We could have done something similar using pdb.post_mortem() with the traceback object:
>>> import sys >>> pdb.post_mortem(sys.last_traceback) > /Users/ashwini/work/instamojo/pdb-mupy/script.py(7)divide() -> return numerator / denominator (Pdb)Similarly we can handle the current exception being handled using pdb.post_mortem() without any argument:
>>> try: ... script.divide(10, 0) ... except Exception: ... pdb.post_mortem() ... > /Users/ashwini/work/instamojo/pdb-mupy/script.py(7)divide() -> return numerator / denominator (Pdb) Basic pdbcommands(Pdb) prompt we have seen so far is pdb’s own shell and it has its own set of commands that makes debugging even easier. In this section we will go through some of the basic commands.
Before starting with the commands it is important to understand the notation we use for commands, for example a command like c(ont(inue)) means we can either use c , cont or continue for this command. The square brackets( [] ) followed by a command are its optional arguments, without square brackets it is a compulsory argument. h(elp) [command] help or si