Now that we have discussed basic debugging methods that will always work, we will look at interactive debugging for some more advanced debugging techniques. The previous debugging methods made variables and stacks visible through modifying the code and/or foresight. This time around, we will look at a slightly smarter method, which constitutes doing the same thing interactively, but once the need arises.
When testing some Python code, you may have used the interactive console a couple of times, since it's a simple yet effective tool for testing your Python code. What you might not have known is that it is actually simple to start your own shell from within your code. So, whenever you want to drop into a regular shell from a specific point in your code, that's easily possible:
import code def spam(): eggs = 123 print('The begin of spam') code.interact(banner='', local=locals()) print('The end of spam') print('The value of eggs: %s' % eggs) if __name__ == '__main__': spam()
When executing that, we will drop into an interactive console halfway:
# python3 test_code.py The begin of spam >>> eggs 123 >>> eggs = 456 >>> The end of spam The value of eggs: 123
To exit this console, we can use ^d (Ctrl + d) on Linux/Mac systems and ^z (Ctrl + Z) on Windows systems.
One important thing to note here is that the scope is not shared between the two. Even though we passed along locals()
to share the local variables for convenience, this relation is not bidirectional. The result is that even though we set eggs
to 456
in the interactive session, it does not carry over to the outside function. You can modify variables in the outside scope through direct manipulation (for example, setting the properties) if you wish, but all variables declared locally will remain local.
When it comes to actually debugging code, the regular interactive console just isn't suited. With a bit of effort, you can make it work, but it's just not all that convenient for debugging since you can only see the current scope and can't jump around the stack easily. With pdb
(Python debugger), this is easily possible. So let's look at a simple example of using pdb
:
import pdb def spam(): eggs = 123 print('The begin of spam') pdb.set_trace() print('The end of spam') print('The value of eggs: %s' % eggs) if __name__ == '__main__': spam()
This example is pretty much identical to the one in the previous paragraph, except that this time we end up in the pdb
console instead of a regular interactive console. So let's give the interactive debugger a try:
# python3 test_pdb.py The begin of spam > test_pdb.py(8)spam() -> print('The end of spam') (Pdb) eggs 123 (Pdb) eggs = 456 (Pdb) continue The end of spam The value of eggs: 456
As you can see, we've actually modified the value of eggs
now. In this case, we used the full continue
command, but all the pdb
commands have short versions as well. So, using c
instead of continue
gives the same result. Just typing eggs
(or any other variable) will show the contents and setting the variable will simply set it, just as we would expect from an interactive session.
To get started with pdb
, first of all, a list of the most useful (full) commands with shorthands is shown here:
It's quite a long list, but you will probably use most of these quite regularly. To highlight one of the options shown in the preceding table, let's demonstrate the setting and use of breakpoints:
import pdb def spam(): print('The begin of spam') print('The end of spam') if __name__ == '__main__': pdb.set_trace() spam()
So far, nothing new has happened, but let's now open the interactive debugging session, as follows:
# python3 test_pdb.py > test_pdb.py(11)<module>() -> while True: (Pdb) source spam # View the source of spam 4 def spam(): 5 print('The begin of spam') 6 print('The end of spam') (Pdb) b 5 # Add a breakpoint to line 5 Breakpoint 1 at test_pdb.py:5 (Pdb) w # Where shows the current line > test_pdb.py(11)<module>() -> while True: (Pdb) c # Continue (until the next breakpoint or exception) > test_pdb.py(5)spam() -> print('The begin of spam') (Pdb) w # Where again test_pdb.py(12)<module>() -> spam() > test_pdb.py(5)spam() -> print('The begin of spam') (Pdb) ll # List the lines of the current function 4 def spam(): 5 B-> print('The begin of spam') 6 print('The end of spam') (Pdb) b # Show the breakpoints Num Type Disp Enb Where 1 breakpoint keep yes at test_pdb.py:5 breakpoint already hit 1 time (Pdb) cl 1 # Clear breakpoint 1 Deleted breakpoint 1 at test_pdb.py:5
That was a lot of output, but it's actually not as complex as it seems:
source spam
command to see the source for the spam
function.print
statement, which we used to place a breakpoint (b 5
) at line 5.w
command.c
to continue up to the next breakpoint.w
again to confirm that.ll
.b
.cl 1
with the breakpoint number from the previous command.It all seems a bit complicated in the beginning, but you'll see that it's actually a very convenient way of debugging once you've tried a few times.
To make it even better, this time we will execute the breakpoint only when eggs = 3
. The code is pretty much the same, although we need a variable in this case:
import pdb def spam(eggs): print('eggs:', eggs) if __name__ == '__main__': pdb.set_trace() for i in range(5): spam(i)
Now, let's execute the code and make sure that it only breaks at certain times:
# python3 test_breakpoint.py > test_breakpoint.py(10)<module>() -> for i in range(5): (Pdb) source spam 4 def spam(eggs): 5 print('eggs:', eggs) (Pdb) b 5, eggs == 3 # Add a breakpoint to line 5 whenever eggs=3 Breakpoint 1 at test_breakpoint.py:5 (Pdb) c # Continue eggs: 0 eggs: 1 eggs: 2 > test_breakpoint.py(5)spam() -> print('eggs:', eggs) (Pdb) a # Show function arguments eggs = 3 (Pdb) c # Continue eggs: 3 eggs: 4
To list what we have done:
source
spam, we looked for the line number.eggs == 3
condition.c
. As you can see, the values 0
, 1
, and 2
are printed as normal.3
. To verify this we used a
to see the function arguments.All of these have been manual calls to the pdb.set_trace()
function, but in general, you are just running your application and not really expecting issues. This is where exception catching can be very handy. In addition to importing pdb
yourself, you can run scripts through pdb
as a module as well. Let's examine this bit of code, which dies as soon as it reaches zero division:
print('This still works') 1/0 print('We shouldnt reach this code')
If we run it using the pdb
parameter, we can end up in the Python Debugger whenever it crashes:
# python3 -m pdb test_zero.py > test_zero.py(1)<module>() -> print('This still works') (Pdb) w # Where bdb.py(431)run() -> exec(cmd, globals, locals) <string>(1)<module>() > test_zero.py(1)<module>() -> print('This still works') (Pdb) s # Step into the next statement This still works > test_zero.py(2)<module>() -> 1/0 (Pdb) c # Continue Traceback (most recent call last): File "pdb.py", line 1661, in main pdb._runscript(mainpyfile) File "pdb.py", line 1542, in _runscript self.run(statement) File "bdb.py", line 431, in run exec(cmd, globals, locals) File "<string>", line 1, in <module> File "test_zero.py", line 2, in <module> 1/0 ZeroDivisionError: division by zero Uncaught exception. Entering post mortem debugging Running 'cont' or 'step' will restart the program > test_zero.py(2)<module>() -> 1/0
The commands
command is a little complicated but very useful. It allows you to execute commands whenever a specific breakpoint is encountered. To illustrate this, let's start from a simple example again:
import pdb def spam(eggs): print('eggs:', eggs) if __name__ == '__main__': pdb.set_trace() for i in range(5): spam(i)
The code is simple enough, so now we'll add the breakpoint and the commands, as follows:
# python3 test_breakpoint.py > test_breakpoint.py(10)<module>() -> for i in range(3): (Pdb) b spam # Add a breakpoint to function spam Breakpoint 1 at test_breakpoint.py:4 (Pdb) commands 1 # Add a command to breakpoint 1 (com) print('The value of eggs: %s' % eggs) (com) end # End the entering of the commands (Pdb) c # Continue The value of eggs: 0 > test_breakpoint.py(5)spam() -> print('eggs:', eggs) (Pdb) c # Continue eggs: 0 The value of eggs: 1 > test_breakpoint.py(5)spam() -> print('eggs:', eggs) (Pdb) cl 1 # Clear breakpoint 1 Deleted breakpoint 1 at test_breakpoint.py:4 (Pdb) c # Continue eggs: 1 eggs: 2
As you can see, we can easily add commands to the breakpoint. After removing the breakpoint, these commands obviously won't be executed anymore.
While the generic Python console is useful, it can be a little rough around the edges. The IPython console offers a whole new world of extra features, which make it a much nicer console to work with. One of those features is a more convenient debugger.
First, make sure you have ipdb
installed:
pip install ipdb
Next, let's try the debugger again with our previous script. The only small change is that we now import ipdb
instead of pdb
:
import ipdb def spam(eggs): print('eggs:', eggs) if __name__ == '__main__': ipdb.set_trace() for i in range(3): spam(i)
Then we execute it:
# python3 test_ipdb.py > test_ipdb.py(10)<module>() 9 ipdb.set_trace() ---> 10 for i in range(3): 11 spam(i) ipdb> b spam # Set a breakpoint Breakpoint 1 at test_ipdb.py:4 ipdb> c # Continue (until exception or breakpoint) > test_ipdb.py(5)spam() 1 4 def spam(eggs): ----> 5 print('eggs:', eggs) 6 ipdb> a # Show the arguments eggs = 0 ipdb> c # Continue eggs: 0 > test_ipdb.py(5)spam() 1 4 def spam(eggs): ----> 5 print('eggs:', eggs) 6 ipdb> # Repeat the previous command, so continue again eggs: 1 > test_ipdb.py(5)spam() 1 4 def spam(eggs): ----> 5 print('eggs:', eggs) 6 ipdb> cl 1 # Remove breakpoint 1 Deleted breakpoint 1 at test_ipdb.py:4 ipdb> c # Continue eggs: 2
The commands are all the same, but the output is just a tad more legible in my opinion. The actual version also includes syntax highlighting, which makes the output even easier to follow.
In short, you can just replace pdb
with ipdb
in most situations to simply get a more intuitive debugger. But I will give you the recommendation as well, to the ipdb
context manager:
import ipdb with ipdb.launch_ipdb_on_exception(): main()
This is as convenient as it looks. It simply hooks ipdb
into your exceptions so that you can easily debug whenever needed. Combine that with a debug flag to your application to easily allow debugging when needed.
pdb
and ipdb
are just two of the large number of debuggers available for Python. Some of the currently noteworthy debuggers are as follows:
There are many others, of course, and there isn't a single one that's the absolute best. As is the case with all tools, they all have their advantages and their fallacies, and the one that is best for your current purpose can be properly decided only by you. Chances are that your current Python IDE already has an integrated debugger.
In addition to debugging when you encounter a problem, there are times when you simply need to keep track of errors for later debugging. Especially when working with remote servers, these can be invaluable to detect when and how a Python process is malfunctioning. Additionally, these services offer grouping of errors as well, making them far more useful than a simple e-mail-on-exception type of script, which can quickly spam your inbox.
A nice open source solution for keeping track of errors is sentry
. If you need a full-fletched solution that offers performance tracking as well, then Opbeat and Newrelic are very nice solutions; they offer both free and paid versions. Note that all of these also support tracking of other languages, such as JavaScript.