Debugging Python Code

When I started learning Python, my super-scientific investigation method for debugging scripts was to insert print(my_variable) into my code (and occasionally printing my_variable.__dict__ where I expected that method to be implemented), run tests locally, and check out what got printed.

Soon, a more experienced colleague of mine introduced me to the pdb debugger in Python, and it was definitely a game changer. The fact that you can inspect your code interactively, line by line, can be extremely useful, and I regularly use it while writing tests for my code.

Note: PyCharm and other IDEs can provide advanced, user-friendly interfaces for debugging. However, I don’t think that what they can offer is substantially different from the built-in debugging options. I use pdb++ which is an extension of the built-in pdb package - it adds syntax highlighting, auto-complete, and some additional commands, but otherwise it’s the same as pdb. You can install it with:
$ pip install pdbpp

At the problematic code part where you suspect a bug, you can just type import pdb; pdb.set_trace() (or the built-in breakpoint() if you’re above 3.7), save the file and run some tests. Execution will stop at that line (called the breakpoint), and you will be dropped into an interactive pdb session.
The following commands can come in handy during that session.

  • c - continue => continue until the next breakpoint() call. Pretty straightforward!
  • s - step => execute the current line, and if it’s a function call, step into the function definition. This can come very handy if you aren’t sure where your bug is occurring.
  • n - next => execute the current line, without stepping into the called function.
  • until => continue execution until a line number great that the current line is reached. This is useful if you encounter a loop (for or while) when debugging some code, eg. if the loop has 100 iterations, you would have to press n quite a few times to reach the loop’s end. You can use u as a shortcut to let the loop finish, and it will stop ant the first line after the loop.
# pdb_test.py
def loop():
    breakpoint()
    # with the `n` command, we would have to work our way through the loop - not great!
    for i in range(100):
        print(f'We are at iteration {i+1}')
    print('We are outside the loop now')
 🍰   python pdb_test.py 
[1] > /Users/samu/samuelbalogh.github.io/pdb_test.py(3)loop()
-> for i in range(100):
(Pdb++) n
[1] > /Users/samu/samuelbalogh.github.io/pdb_test.py(4)loop()
-> print(f'We are at iteration {i+1}')
(Pdb++)  n
We are at iteration 1
[1] > /Users/samu/samuelbalogh.github.io/pdb_test.py(3)loop()
-> for i in range(100):
(Pdb++) n
[1] > /Users/samu/samuelbalogh.github.io/pdb_test.py(4)loop()
-> print(f'We are at iteration {i+1}')
(Pdb++) 
We are at iteration 2
[1] > /Users/samu/samuelbalogh.github.io/pdb_test.py(3)loop()
-> for i in range(100):
(Pdb++) n
[1] > /Users/samu/samuelbalogh.github.io/pdb_test.py(4)loop()
-> print(f'We are at iteration {i+1}')
(Pdb++) 

and so on - however, with u, you can just skip the loop:

[1] > /Users/samu/samuelbalogh.github.io/pdb_test.py(4)loop()
-> print(f'We are at iteration {i+1}')
(Pdb++) u
[0] > /Users/samu/samuelbalogh.github.io/pdb_test.py(8)<module>()
-> loop()
(Pdb++) n
We are at iteration 3
We are at iteration 4
We are at iteration 5
...
We are at iteration 98
We are at iteration 99
We are at iteration 100
We are outside the loop now
--Return--
[0] > /Users/samu/samuelbalogh.github.io/pdb_test.py(8)<module>()->None
-> loop()
(Pdb++) 
  • until can receive an argument, in which case execution will continue until reaching that line.

  • display is a pretty neat command if you are wanting to track down where a variable is mutated. You can give it an expression, and it displays the value of the expression if it changed, each time execution stops. In this example, we are tracking the value of the variable called number. The debugger shows what it changed to, and what was the previous value - pretty neat!

import random

def loop():
    breakpoint()
    number = 0
    for i in range(100):
        print(f'We are at iteration {i+1}')
        number += random.randint(0, 100)
    print('We are outside the loop now') 

Notice the number: <undefined> --> 0 and number: 0 --> 76 parts in the pdb console, which tell you the sequences of events:

 🍰   python pdb_test.py 
[1] > /Users/samu/samuelbalogh.github.io/pdb_test.py(5)loop()
-> number = 0
(Pdb++) display number
(Pdb++) n
[1] > /Users/samu/samuelbalogh.github.io/pdb_test.py(6)loop()
-> for i in range(100):
number: <undefined> --> 0
(Pdb++) 
[1] > /Users/samu/samuelbalogh.github.io/pdb_test.py(7)loop()
-> print(f'We are at iteration {i+1}')
(Pdb++) 
We are at iteration 1
[1] > /Users/samu/samuelbalogh.github.io/pdb_test.py(8)loop()
-> number += random.randint(0, 100)
(Pdb++) 
[1] > /Users/samu/samuelbalogh.github.io/pdb_test.py(6)loop()
-> for i in range(100):
number: 0 --> 76
(Pdb++) 
  • pp [expression] => pretty-prints the value of a variable. Useful in combination with __dict__ to make the output more human-readable (pp my_object.__dict__)
  • source [expression] => tries to get the source code of [expression] - neat if you don’t want to leave your terminal and quickly want to retrieve a function/class definition.

There are a lot of other useful commands provided by pdb - but these are the ones that I’m using most often. Checkout out the additional sources for more details.

Happy debugging!

# Additional resources

Written on July 5, 2020

If you notice anything wrong with this post (factual error, rude tone, bad grammar, typo, etc.), and you feel like giving feedback, please do so by contacting me at samubalogh@gmail.com. Thank you!