My Pdb++ config and extensions

In the present era of software development no one needs to be convinced about the benefits of interactive tooling for programming languages. Since eternity, Python shipped with a built-in GDB-style command-line debugger, Pdb, as well as a base debugger interface, bdb. Pdb though, is rather barebones, lacking tab completion and many other usability features. It also can shadow Python identifiers, which is extremely annoying, for example list(x) will not invoke the list constructor but the list Pdb command, and one needs to do p list(x) to avoid that.

The Jupyter project came up with its ipdb, which ships with IPython. My debugger of choice, however, is Pdb++, which I started using because, unlike ipdb, it didn’t fail when used in Pytest tests with stdout capture turned on.

Unlike ipdb, Pdb++ hijacks the built-in pdb.Pdb class, which has its positive and negative sides. Positives being it “just working” with any tooling which invokes the stock pdb, while the negatives being potentially introducing crashes in these tools, due some of Pdb++ overrides violating the Liskov substitution principle.

This article will focus on the way I use Pdb++, not whether or not one should use it.

Configured Pdb++

Configured Pdb++

My Pdb++ config - annotated

Pdb++ can be configured by the ~/.pdbrc.py file. Since it’s a normal Python file, found and imported by Pdb++, it gives you a lot of power of customisation.

The snippets available below depend on the following imports:

import inspect
from pdb import DefaultConfig, Pdb
from pprint import pprint

The config object

class Config(DefaultConfig):

    prompt = '(Moo++) '
    sticky_by_default = True
    # The 256 colour formatter prints very dark function names:
    use_terminal256formatter = False

The config class is rather self-explanatory. sticky_by_default is a huge usability feature - think of it as automatic code listing. Extremely helpful when navigating the stack up and down, and advancing through code. The changed prompt is non-essential, although it helps identify that other Pdb customisations are in place.

The displayhook

def displayhook(self, obj):
    """If the type defines its own pprint method, use it on its instances."""
    pprint_impl = getattr(obj, 'pprint', None)
    if (
        pprint_impl is not None and
        inspect.ismethod(pprint_impl) and
        pprint_impl.__self__ is not None  # Only call if bound to an object.
    ):
        pprint_impl()
    else:
        pprint(obj)

Pdb.displayhook = displayhook

This snippet enables pretty-printing of all values by default. Regular printing can still be achieved by the p, or print command.

Pdb.displayhook = displayhook monkey-patches the displayhook on the Pdb class already replaced by Pdb++.

Quality of life commands

def do_findtest(self, arg):
    """Find the closest function starting with 'test_', upwards the stack."""
    frames_up = list(reversed(list(enumerate(self.stack[0:self.curindex]))))
    for i, (frame, _) in frames_up:
        if frame.f_code.co_name.startswith('test_'):
            self.curindex = i
            self.curframe = frame
            self.curframe_locals = self.curframe.f_locals
            self.print_current_stack_entry()
            self.lineno = None
            return


def do_bottommost(self, arg):
    """Go to the bottommost frame."""
    last_frame = self.stack[-1][0]
    last_index = len(self.stack) - 1

    self.curindex = last_index
    self.curframe = last_frame
    self.curframe_locals = self.curframe.f_locals
    self.print_current_stack_entry()
    self.lineno = None
    return

Pdb.do_findtest = do_findtest
Pdb.do_ft = do_findtest
Pdb.do_bottommost = do_bottommost
Pdb.do_bm = do_bottommost

These commands, registering as findtest or ft and bottommost or bm automate navigating through the stack, most helpful in post-mortem test debugging. findtest walks the stack upwards, until it sees a function that looks like a test (starting with test_, which is a Pytest convention), while the bottommost command can be used return to the error site after using findtest, or simply go down the stack, to the debugger invocation site or exception origin, no matter what you were doing before.

An anecdote

Long before I published my config here, I shared it with my now former team lead. They took the config and the use of Pdb++ to their next job. Someone noticed it during a presentation and wanted to know more, which resulted in the following exchange:

– Hey, I wanted to ask you about the nice looking Pdb CUI tool you used in the demo.

– Aa. Pdb++? Yeah, I’ve been wanting to add that everywhere for a while.

– Ah ok, I thought I saw ‘Moo++’ or something.