16. Debugging

  • At this stage we have written quite a few algorithms

  • We have also gotten comfortable writing assertion tests for our algorithms

  • By this point, we have also seen plenty of bugs

    • Having bugs in your code is normal

    • Debugging is a large part of programming

    • Finding and fixing bugs is part of the process

  • You have probably already started developing your own debugging habits

  • In this topic, we will look at a few accessible debugging strategies

  • Before getting started, it is worth emphasizing what this is all about

    • It’s not just about getting your program to work

    • It’s about understanding your program and understanding why it is behaving the way it is

../../_images/debugging_bug.png

16.1. The Importance of Debugging

  • As programs get more complex, errors can become harder to track down

  • No matter how experienced you are, debugging remains part of programming

  • Beyond the obvious benefit of fixing your code, debugging helps us improve many important skills:

    • Knowledge of algorithms

    • Knowledge of the programming language

    • The ability to reason logically

    • The ability to reason about flow of execution in code

16.2. Error Messages

  • In many cases, Python will generate an error message giving you an idea of what went wrong

  • Sometimes these error messages are very helpful

line 17
print(5 + 5 +)
            ^
SyntaxError: invalid syntax
  • Other times the error messages are less clear

    • Often, you can search for the error message and find an explanation

  • Common error messages you will come across are

    • Syntax errors

    • Indentation errors

    • Name errors

    • Type errors

    • Index errors

16.2.1. Syntax Errors

  • Syntax errors often produce fairly helpful messages

  • These messages typically tell you where the error is, as seen in the above example

  • Usually, the main job is to read what Python is telling you

Activity

See if you can find all the errors in the following example. You may be able to find them just by looking at the code, but feel free to copy/paste the code into Colab to see what Python reports.

1deff borken(a(
2    a = a + * 3
3    walrus
4    return a + 2
5
6broken(5)

16.2.2. Type Errors

  • As we have seen, Python is pretty good about figuring out types

1a = 5           # It's an integer
2b = 5.5         # Float since there is a decimal
3c = a + b       # Mixing types in the expression (int and float), but no big deal
4print(c)        # Results in 10.5
  • But Python can only do so much

  • For example, Python cannot figure out what it should mean to add an integer and a string together

    • Think about it this way: what would it mean for "Hello" / 32?

1print(99 + "bottles of beer on the wall")
2TypeError: unsupported operand type(s) for +: 'int' and 'str'
  • Sometimes a type-related issue does not generate an error message

  • Consider the inconsistent function defined in the following example

    1def inconsistent(a, b):
    2    return a + b
    
  • There is nothing wrong with this function, but notice what happens with these two calls

    • inconsistent(1, 1) returns 2

    • inconsistent("1", "1") returns "11"

  • It may seem obvious to just avoid the wrong argument types

  • But remember that Python’s input returns a string, even if the user typed digits

    • This is a very common source of bugs

16.3. Logic errors

  • Logic errors can be quite difficult to debug

  • Everything may seem like it’s working, but the program still behaves incorrectly

  • Sometimes the errors may be obvious, like an infinite loop

  • Sometimes they show up only in edge cases, so things work most of the time

  • Most people develop their own habits as they gain experience — a few strategies are discussed below

16.3.1. Print

  • Probably the simplest debugging method is to call print in your code

    • Print out the value of some variable

    • Add a print to see if Python actually executed a specific code block

  • Prints are useful because they let us compare what we expect with what is actually happening

Activity

There is a problem with the following function. It almost works, but it is slightly off. Read the description, see if you can identify the issue, and then use prints to inspect values and track down the problem.

 1def sum_numbers_up_to(n: int) -> int:
 2    """
 3    This function adds up all the numbers from 0 - n exclusively.
 4    Eg. 5 -> 0 + 1 + 2 + 3 + 4 -> 10
 5
 6    :param n: The number we are summing to. Note we do not count n
 7    :return: The sum of the numbers
 8    """
 9
10    total = 0
11    c = 0
12    while c < n:
13        c += 1
14        total += c
15    return total
16
17assert 0 == sum_numbers_up_to(0)
18assert 10 == sum_numbers_up_to(5)
  • Debugging with print often follows a simple pattern

    • Form a hypothesis about the value of a variable at a specific place in your program

    • Add a print to print out the variable’s value

    • Compare your expectation with reality

    • If they matched, perhaps the problem is elsewhere

    • If they do not match, investigate why they differ

  • Each print gives us a little more information

  • Depending on the problem, you may need several prints

16.3.2. Pencil & Paper

  • One challenge with debugging logic errors is avoiding assumptions about your code

    • Do not assume a variable has a specific value

    • Do not assume any specific functionality of a block of code

  • This is easier said than done, especially when you keep rerunning your code

    • Like in the case of using print

  • A good alternative is to not run your program on the computer

  • Instead, execute the program on paper

    • Create a table to keep track of each variable and the current value

    • Execute the program on paper one line at a time, keeping track of the values of the variables

  • The benefit is that it forces you to slow down and be deliberate — assumptions are harder to make when you have to write everything out

  • This can feel tedious, but it often helps

Note

The point here is to take your time and think about every line of code. Rushing through and making assumptions will inevitably cause this strategy to fail — the program is behaving unexpectedly precisely because something you assumed was true, isn’t.

16.3.3. Delta Debugging

  • Another strategy is Delta Debugging

  • Delta Debugging is a more systematic way to isolate where an issue is arising

  • A simple version of the strategy is

    • Comment out every line of code and run it

    • Un-comment out a logical unit of code and run it

      • This should only be a line or two of code

      • You might also add a print

    • Did it do what you expected?

      • No? You found at least part of the problem

      • Yes? Keep looking

  • This mirrors the recommended coding strategy from earlier — write a little, test, repeat

16.3.4. Rubber Duck Debugging

  • Rubber Duck Debugging

  • A surprisingly effective form of debugging

  • One helpful strategy is to explain your code to something

  • Sometimes you have a friend

  • Sometimes you have your mom

  • Sometimes you have a pet

  • And sometimes you have a rubber duck

  • The point is that explaining your code out loud often helps you notice what you skipped over mentally

16.4. For Next Topic