Debugging

Debugging is the process of identifying and removing errors from computer hardware or software.

This guide provides a summary of some approaches you could use.

Using print statements

"The most effective debugging tool is still careful thought, coupled with judiciously placed print statements." Brian Kernighan

Famous programmers who reportedly prefer print statements include Guido van Rossum (Python), Robert C. Martin (author and Agile Manifesto signatory) and Brian W. Kernighan and Rob Pike (UNIX).

See the following example from https://www.alpharithms.com/python-breakpoint-built-in-function-410212/

def find_min(nums: [int]) -> int:
    """Finds the smallest integer value in a list"""
    smallest = 0
    for num in nums:
        print('num:', num, 'smallest:', smallest)  # Print added for debugging
        if num < smallest:
            print('num smaller; re-assigning value:', num)  # Print added for debugging
            smallest = num
    return smallest


if __name__ == '__main__':
    min_value = find_min([9, 6, 3])
    print(min_value)  # Returns 0 when 3 was expected

__repr__ returns a string representation of the object. For many types, this function makes an attempt to return a string that would yield an object with the same value.

Calling __repr__ when using print() can provide clearer detail, so long as the __repr__ method has been defined for a class.

Run the following to see the difference:

from decimal import Decimal

x = Decimal('3.4')
print(x)  # Without repr
print(repr(x))  # With repr

It should print:

3.4
Decimal('3.4')

When writing your own classes remember to include a __repr__ method. Run the following code to see the difference:

class PersonA:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __repr__(self):
        return f'{self.__class__.__name__}('f'{self.name!r}, {str(self.age)})'


class PersonB:
    def __init__(self, name, age):
        self.name = name
        self.age = age


if __name__ == '__main__':
    pa = PersonA('Olive', 6)
    pb = PersonB('Arthur', 6)
    print(repr(pa))  # __repr__ defined
    print(repr(pb))  # __repr__ not defined

This prints:

PersonA('Olive', 6)
<__main__.PersonB object at 0x7fccffd88748>

Breakpoints

Breakpoints are a location or condition under which we want the execution of code to be suspended. You can then inspect:

Pdb and breakpoint()

Python 3 is packaged with the Pdb debugger.

"It supports setting (conditional) breakpoints and single stepping at the source line level, inspection of stack frames, source code listing, and evaluation of arbitrary Python code in the context of any stack frame. It also supports post-mortem debugging and can be called under program control."

Pdb commands can be added to Python code and the results output to command line/terminal.

The typical usage to break into the debugger is to insert the following line into code to start the debugger:

import pdb;

pdb.set_trace()

For example:

def find_min(nums: [int]) -> int:
    smallest = 0
    import pdb;
    pdb.set_trace()  # Enter the Python debugger
    for num in nums:
        if num < smallest:
            smallest = num
    return smallest


if __name__ == '__main__':
    min_value = find_min([9, 6, 3])
    # Should print 3; instead prints 0
    print(min_value)

This is more conveniently called by using the Python breakpoint() function which calls pdb.set_trace(), e.g.

def find_min(nums: [int]) -> int:
    smallest = 0
    breakpoint()  # Enter the Python debugger
    for num in nums:
        if num < smallest:
            smallest = num
    return smallest

When pdb is entered, you can enter commands to walk through the code e.g.

n(ext) - Continues execution of program until the next line is reached, or whether the current function returns.

c(cont(inue)) - Continues execution of our program until it reaches the next pdb.set_trace()

r(eturn) - Continues execution until the current function returns

s(tep) - Executes the current line and stops at the first possible occasion.

w(here) - Prints out a stack trace.

l(ist) - Lists the source code for the current file. With no arguments this equates to the 11 lines surrounding current line.

q(uit) - Quits the current debugging session.

For a full list of debugger commands have a look at the official documentation.

If you execute the example code then enter the following commands when Pdb starts:

pdb screenshot in PyCharm

Interactive debugging in an IDE

Advantages of interactive debuggers:

To learn how to use these you will need to explore tutorials relevant to the IDE:

Using the debugger with pytest

pdb can be combined with pytest to aid debugging during testing.

Configure verbose test reports e.g. pytest --v to generate more detailed test results.

Use pytest with pdb with breakpoints: e.g.

# use the the Python debugger and report on every failure
pytest --pdb 

# drop into pdb on the first failure, then end test session
pytest -x --pdb 

# drop into pdb before the tests and you can then step through each breakpoint to the end of the test
pytest test_login.py --trace 

This guide suggests starting with print(), and if that doesn't help identify the problem move on to adding breakpoints and use pdb.