Python Debugging: How to use pdb.pm()

Python’s debugger, pdb, is a great tool and one of my favorite uses of pdb is pdb.pm(). This post shows how to use pdb.pm() by working through an example of a buggy implementation of mergesort.

pdb.pm() launches pdb to debug the most recently raised exception. When you find yourself saying “Shit! That exception occurred four layers deep in code I wasn’t editting and setting up the state to reproduce this is going to take 20 minutes” or “I wish I could just debug right from where the exception was raised”, pdb.pm() is what you want.

As a working example, consider the following implementation of mergesort. It isn’t great: the code has a logic bug, duplicates memory and suffers from array expansion, we will only focus on the logic bug.

def mergesort(xs): 
    if len(xs) == 1: 
        return xs 
    else: 
        left = mergesort(xs[:len(xs)//2]) 
        right = mergesort(xs[len(xs)//2:]) 
        ys = [] 
        left_i, right_i = 0, 0 
        while left_i < len(left) or right_i < len(right): 
            if left[left_i] <= right[right_i]: 
                ys.append(left[left_i]) 
                left_i += 1 
            else: 
                ys.append(right[right_i]) 
                right_i += 1 
        return ys 

The bug is in the while loop. If left_i is less than len(left) it will drop into the loop body, even if right_i is greater than or equal to len(right). In that case, it will try to index out of the array bounds. Gasp!

If you try to use this code, you will get the error we expect.

In [2]: mergesort([6,5,4,3,2,1])                                                                                                                                                                                  
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-30-15081790cedb> in <module>
----> 1 mergesort([6,5,4,3,2,1])

<ipython-input-28-cb4e97206eb8> in mergesort(xs)
      3         return xs
      4     else:
----> 5         left = mergesort(xs[:len(xs)//2])
      6         right = mergesort(xs[len(xs)//2:])
      7         ys = []

<ipython-input-28-cb4e97206eb8> in mergesort(xs)
      4     else:
      5         left = mergesort(xs[:len(xs)//2])
----> 6         right = mergesort(xs[len(xs)//2:])
      7         ys = []
      8         left_i, right_i = 0, 0

<ipython-input-28-cb4e97206eb8> in mergesort(xs)
      8         left_i, right_i = 0, 0
      9         while left_i < len(left) or right_i < len(right):
---> 10             if left[left_i] <= right[right_i]:
     11                 ys.append(left[left_i])
     12                 left_i += 1

IndexError: list index out of range

While we already know what the bug is, we can use pdb.pm() to poke at the state to see what is going on. We can query for the state of variables to see what triggered the problem.

In [33]: import pdb; pdb.pm()                                                                                                                                                                                      
> <ipython-input-28-cb4e97206eb8>(10)mergesort()
-> if left[left_i] <= right[right_i]:
(Pdb) left_i, right_i
(0, 1)
(Pdb) left
[5]
(Pdb) right
[4]

Ok, now we can see what happened. Both left and right had a single element but the code tried to index the non-existent second element in right.

If we needed to, we could step up the traceback to understand the state using the command u (for up). Then we can poke at the state higher in the stack as we did before.

(Pdb) u
> <ipython-input-28-cb4e97206eb8>(6)mergesort()
-> right = mergesort(xs[len(xs)//2:])
(Pdb) left
[6]
(Pdb) xs
[6, 5, 4]
(Pdb) xs[len(xs)//2:]
[5, 4]

Debugging exceptions becomes a lot easier once you become comfortable with pdb.pm().

Written on July 5, 2020