Personal Blog of Rob Sayers

How to supercharge your awful print style debugging with breakpoint()

I'll admit that I'm guilty of not using a debugger religiously with Python. Don't get me wrong, I do use a proper debugger for my Python code. There are times however when I feel the standard debugger workflow isn’t the best option.

In an ideal world, our code would be easily debuggable wherever it runs. Unfortunately we often have to adapt to less than ideal situations. Is it a great idea to debug live code on a server? Maybe not, but it might be the best option at the moment. I’ll leave the justifications up to you.

“Print debugging” or whatever variant of the term you choose you use refers to sprinkling print statements throughout your code to examine what is happening as it executes. Imagine you have a function called get_object(), you have no idea what this awfully named function returns, so you might add some print statements like:

def do_something():
    obj = get_object()
    print(obj)
    print(str(obj))
    print(obj.__dict__)
    return False
    #  Followed by the code that would normally process obj

And when running the script, you will see something like:

<__main__.TestObject object at 0x10a1c3f70>
<__main__.TestObject object at 0x10a1c3f70>
{'foo': 1, 'bar': 2, 'message': 'Hello from <__main__.TestObject object at 0x10a1c3f70>'}

But behold the magic of breakpoint():

def do_something():
    obj = get_object()
    breakpoint()
    # Followed by the code that would normally process obj

Now when we run, we'll be dropped to a prompt:

--Return--
> /Users/rsayers/development/example/test.py(13)do_something()->None
-> breakpoint()
(Pdb)

This prompt is a Python repl, running in the spot where you placed breakpoint(). Now you can check out obj and gather the information you need from there without a lot of ugly print statements or constant rerunning of code.

(Pdb) dir(obj)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bar', 'foo', 'message']
(Pdb) obj.__class__
<class '__main__.TestObject'>
(Pdb)

Not only can you explore the local environment at a spot of your choosing, but you can alter the program state, and then allow it to continue with your new data. Consider the following:

def print_message():
    message = 'This is a test'
    print(message)

That's a pretty simple function that does exactly what you'd expect. Let's set a breakpoint, and use that to change the message at runtime.

First we need to add our breakpoint:

def print_message():
    message = 'This is a test'
    breakpoint()
    print(message)

And now when run, we can make live changes

> /Users/rsayers/development/example/test.py(4)print_message()
-> print(message)
(Pdb) message = 'I have altered the message!'
(Pdb) cont
I have altered the message!

Moving forward, you can always use the built in help command in the debugger:

Documented commands (type help <topic>):
========================================
EOF    c          d        h         list      q        rv       undisplay
a      cl         debug    help      ll        quit     s        unt
alias  clear      disable  ignore    longlist  r        source   until
args   commands   display  interact  n         restart  step     up
b      condition  down     j         next      return   tbreak   w
break  cont       enable   jump      p         retval   u        whatis
bt     continue   exit     l         pp        run      unalias  where

Miscellaneous help topics:
==========================
exec  pdb

Since breakpoint() is shorthand for running our debugger at the spot of our choosing, The official Python Debugger documentation will help you really dig deep into this topic.