Chapter06 Debugging Pytest Pdb

If you find this content useful, consider buying this book:

If you enjoyed this book considering buying a copy

Chapter 6: Debugging with Pytest #

Noah Gift

The real world mostly works. When you get in a modern car, it seldom breaks down. Most houses in the United States are well built and don’t require daily repair. In school, most tests and quizzes have a passing grade by design. Getting an “F” grade is typically very rare.

Software engineering is quite different. Everything breaks every day. When learning to be a software developer, this is the biggest challenge to overcome. Beginners feel like a one hundred foot wave is crushing them over and over again. One solution to this is adopting a new mindset.

I tell students new to programming to embrace the failure. When you are getting hammered by mistakes and failures, this means you are exactly on the correct path. If no errors appear, then no progress is made in learning. When a student has stopped feeling frustrated, this is an awful sign; it means that education has stopped. Taking joy in constant mistakes is a secret technique to accelerate the journey to mastery.

How to debug code #

One of the joys of programming in Python is that it seldom requires a complex solution to debug code. Let’s walk through some of the options on how to debug code. If you are toying around with some sample code, then a print statement is often the best way to debug something.

Here is a good example. When writing a function, one of the easy things to mess up is the input. Here is an example of how to debug a simple task by using python f-strings to print the values out.

def add(x,y):
    print(f"The value of x: {x}")
    print(f"The value of y: {y}")
    return x+y

result = add(3,7)
print(result)

When run it returns the following:

> python hello_debug.py
The value of x: 3
The value of y: 7
10

It is relatively straight forward to catch something wacky. Let’s suppose that someone tried to pass in an object instead of an int.

class Z: pass
zz = Z()

def add(x,y):
    print(f"The value of x: {x}")
    print(f"The value of y: {y}")
    return x+y

result = add(3,zz)
print(result)

Testing itself would catch this, but print statements add a lot of clarity.

(.tip) ➜  chapter6 git:(master) ✗ python hello_debug_object.py
The value of x: 3
The value of y: <__main__.Z object at 0x1089df6d0>
Traceback (most recent call last):
  File "hello_debug_object.py", line 9, in <module>
    result = add(3,zz)
  File "hello_debug_object.py", line 7, in add
    return x+y
TypeError: unsupported operand type(s) for +: 'int' and 'Z'

The code blows up when run, but before it blows up, it gives some constructive feedback. The following print statement should instantly cause the programmer to slap their forehead. You cannot add an object to an int.

The value of y: <__main__.Z object at 0x1089df6d0>

Using a debugger #

What if the problem still isn’t apparent after adding print statements? This step does occasionally happen with Python. My go-to weapon when a print statement doesn’t work is to put in “one-liner” that invokes the python debugger pdb.

Let’s watch this in action. To insert the debugger into Python code you only need to specify import pdb;pdb.set_trace(). Next, you can see that the debugger line inserted.

class Z: pass
zz = Z()

def add(x,y):
    import pdb;pdb.set_trace()
    print(f"The value of x: {x}")
    print(f"The value of y: {y}")
    return x+y

result = add(3,zz)
print(result)

When the code invokes, it stops inside the function. This step is helpful because it allows us to “step” line by line into the system. The entry point will enable me to print x by typing x. I can see the value is 3. I press n on the keyboard, which brings me into the next line of code. When I type y, I can see it is an object, which is a problem. I can then exit by typing q.

(.tip)   chapter6 git:(master)  python hello_debug_object_pdb.py
> /Users/noahgift/testing-in-python/chapter6/hello_debug_object_pdb.py(6)add()
-> print(f"The value of x: {x}")
(Pdb) x
3
(Pdb) n
The value of x: 3
> /Users/noahgift/testing-in-python/chapter6/hello_debug_object_pdb.py(7)add()
-> print(f"The value of y: {y}")
(Pdb) y
<__main__.Z object at 0x103c32310>
(Pdb) q
Traceback (most recent call last):
  File "hello_debug_object_pdb.py", line 10, in <module>
    result = add(3,zz)
  File "hello_debug_object_pdb.py", line 7, in add
    print(f"The value of y: {y}")
  File "hello_debug_object_pdb.py", line 7, in add
    print(f"The value of y: {y}")
  File ".../Versions/3.7/lib/python3.7/bdb.py", line 88, in trace_dispatch
    return self.dispatch_line(frame)
  File ".../Versions/3.7/lib/python3.7/bdb.py", line 113, in dispatch_line
    if self.quitting: raise BdbQuit
bdb.BdbQuit

Using the Python debugger is a powerful debugging technique, and as you can see very straightforward. If you find yourself stuck in a severe debugging problem, it is the first place to look. What is nice is that it also integrates well with pytest. This step can be an incredible combination.

Python Debugger (PDB) integration #

With this knowledge of how pdb works, let’s integrate this with pytest. First, a test class creates named test_debug.py

from hello_debug import add
import pytest

@pytest.fixture
def myobj():
    class Foo():pass
    return Foo()

def test_add():
    assert add(1,2) == 3

def test_add_object(myobj):
    assert add(1,myobj) == 3

You can see that a fixture defines an action that returns an object. An object will not work in the test_add_object test case, as shown earlier. You cannot add an int and a purpose. To debug with pytest run the following flag:

python -m pytest --pdb

This command then both runs the test but allows a breakpoint in the code after the failed test shows.

python -m pytest --pdb
============================= test session starts ==============================
platform darwin -- Python 3.7.6, pytest-5.3.2, py-1.8.1, pluggy-0.13.1
rootdir: /Users/noahgift/src/testing-in-python/chapter6
plugins: cov-2.8.1
collected 2 items

test_debug.py .F
The value of x: 1
The value of y: <test_debug.myobj.<locals>.Foo object at 0x105b5fc50>

myobj = <test_debug.myobj.<locals>.Foo object at 0x105b5fc50>

    def test_add_object(myobj):
>       assert add(1,myobj) == 3

test_debug.py:13:
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

x = 1, y = <test_debug.myobj.<locals>.Foo object at 0x105b5fc50>

    def add(x,y):
        print(f"The value of x: {x}")
        print(f"The value of y: {y}")
>       return x+y
E       TypeError: unsupported operand type(s) for +: 'int' and 'Foo'

hello_debug.py:4: TypeError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

>>>>>>>>>>>>>>>> PDB post_mortem (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>
> /Users/noahgift/src/testing-in-python/chapter6/hello_debug.py(4)add()
-> return x+y
(Pdb) x
1
(Pdb) y
<test_debug.myobj.<locals>.Foo object at 0x105b5fc50>

What is nice is that the same commands that regular pdb uses are available. I can dig into this code example and also quickly identify that this object is the problem.