Debugging is the most frustrating aspect of coding, especially when you don’t know what’s going on or where the bug is stemming from. In this section we’re going to talk about how to effectively find bugs.
In the very first week, I brought up the fact that coding is not similar to writing. Unlike writing an essay, you should test every function as soon as you write it.
That makes sense conceptually, but how do you actually test a function?
You give a function various input cases and you verify that the output matches.
Python has a special function called
assert. It takes a boolean expression. If the boolean expression evaluates to
True, then everything proceedes as expected. But if it evaluates to
False, then the whole program stops at that statement.
a = 5 assert(a == 4) print "Yay passed all tests!"
When we run this trivial example, we get the following output
Traceback (most recent call last): File "stuff.py", line 2, in <module> assert(a == 4) AssertionError
Here we see a new kind of error, an
AssertionError, which is what happens when an
assert statement evaluates to
Let’s imagine you’re doing the Pong lab once more. We have a function telling us whether a paddle is out of the bounds of the screen or not. Let’s test it.
def out_of_bounds(xPos, yPos, WIDTH, LENGTH): # Some complicated operation pass
Pretty simple function. How do we test it? We give it sample input cases. Often times, insufficient testing of a function leads you to believe a function works, but this is just because you haven’t tested it in special cases, so to be careful, we have to test all the edge cases.
Imagine the screen is
300 x 300 pixels. This would be a sample test for this function.
PADDLE_WIDTH = 20 PADDLE_LENGTH = 40 # Case 0 - Left paddle on screen assert(not out_of_bounds(0, 150, PADDLE_WIDTH, PADDLE_LENGTH)) # Case 1 - Left paddle touching top of screen assert(out_of_bounds(0, 0, PADDLE_WIDTH, PADDLE_LENGTH)) # Case 2 - Left paddle touching bottom of screen assert(out_of_bounds(0, 260, PADDLE_WIDTH, PADDLE_LENGTH)) # Case 3 - Beyond end of screen assert(out_of_bounds(0, 300, PADDLE_WIDTH, PADDLE_LENGTH))
And something similar for the right paddle as well. Why did we choose these particular cases? Everyone can agree on the two obvious cases: inside the screen and outside the screen. Fair enough. The edge case in this circumstance is when the paddle is touching the edge of the screen but not outside of it. Should this return
This depends on the purpose of the function. If the function is used to check whether it’s okay for the paddle to move, then it should return
False. If it’s used simply to check whether it’s out of bounds, then it should return
True. It all depends on what the function is being used for. Either way, your
assert statements should agree with you.
In this case, I’ve decided that this function, if it returns
True, prevents movement of the paddle. So only the first case should return
This is called unit testing, where a test only verifies an individual function.
So when can this fail? This can fail when functions, even though they are correct individually, cannot mesh together.
Imagine you originally wrote the
out_of_bounds function to determine only whether the paddle was out of bounds, but you started using it to determine whether the paddle should be allowed to move in either direction. In this case, the unit tests would not catch this.
This is where print statements come in.
Print statements are the most basic way to debug code. At every point in your code, you should use print statements to verify the variables you’re expecting have acceptable values.
This is used as a verification to make sure things are still running okay (not as a substitute for actually stepping through the code yourself).
But for longer labs, this can get particularly annoying. Imagine you had to type out all these print statements and then remove them all before submitting the lab.
So what do we do when those annoying print statements are too much work?
Eclipse, or IntelliJ if you’re using that, has a debugger. It’s a really heavy-duty debugger that can keep track of everything! All your variables, every function you’ve declared, all in one place. Using this debugger, you can step through your code (remember the program counter – this is what that does) and manually examine what’s going on.
Using the debugger is a very useful skill to learn, and it will greatly benefit you. Let's look at an example of buggy code.
# how_rich.py # Computes the balance of a bank account that started with $1.00 in year 0 # and had interest compounded annually through 2014. INITIAL_BALANCE = 1.0 # how much the account starts with INTEREST_RATE = 5.0 # interest rate, as a percent CURRENT_YEAR = 2014 # when to stop compounding interest balance = INITIAL_BALANCE # current balance, will be updated annually year = 0 # which year def get_balance(initial_amount, interest_rate, time): factor = (1 + interest_rate / 100)**time return initial_amount * factor # Update the balance once per year, up to and including CURRENT_YEAR. for i in range(CURRENT_YEAR): balance = get_balance(INITIAL_BALANCE, INTEREST_RATE, year) # All done. Print the result, nicely formatted. print "At year " + str(CURRENT_YEAR) + ", the balance is " + str(balance) + "."
The code obviously has a bug. But where is it? Let’s run the code and see what happens.
Oops. That can't be right. The first thing to do when we start debugging is set breakpoints. Breakpoints are places where the debugger can stop executing, and freeze your program, so that you can take a closer look at what's going on. You can, of course, resume executing from a breakpoint. A good place to set breakpoints is right before the place you think something is going wrong. In Eclipse, you can right click to the left of the code where the line numbers are to set a breakpoint. Some of you might not have enabled line numbers, and there may just be empty space there; that’s fine, too. Right clicking there will still work..
If you’ve successfully added a breakpoint, it should look like this.
Now we can fire up the debugger!
Your program will run until it reaches line 18, where we set the breakpoint. Then, it will pause.
There’s a lot going on in the image above, so let’s take it piecemeal. The console simply says "pydev debugger: starting". Nothing of interesting here; it just tells you what you already know - that you’re debugging a program. We won’t be looking at this bottom console ever again. The second from the bottom is pretty familiar as well; it has your code, and specifically, it highlights in green the line that it is about to execute. This bears repeating: it is not the line that has already finished, but the one about to execute. Now there are two windows on the top level. Ignore the one on the left; we won’t ever use it in this class. The one on the right is what we really care about.
Make sure you click on the tab that says "variables". Here, we have every variable and its value listed out! No more print statements. In fact, we can just easily and simply follow along now without having to print out everything and scrolling through the console. Once you get to more complex labs which have very many variables, you’ll see why this is so useful. Take a look at the variables. Some of them are not relavent to us, like
__name__, but variables like
INTEREST_RATE are obviously very important.
Great. So how do we continue? Remember, we haven't completed any iterations of our for loop because the debugger stopped the very moment it hit line 18 for the first time. As it turns out, there are two different ways in which we can progress.
The first is called stepping into. Here’s a rule of thumb: Only ever step into a function that you wrote. What does stepping into mean? If line 18 was a function call, and you think that something is going wrong inside that function and you want to take a look with the debugger, then we step into that function. You do not step into code that you did not write because it will not make any sense.
Here, the actual for loop itself is quite simple, and it is not likely that the for loop is wrong. So, it must mean that our
get_balance function is wrong, so we will step into that function.
Now we are inside the
get_balance method. Notice the updated variables panel; it has only the variables local to this function. Of course, if we wanted to look at the global variables, we would simply expand the globals tab at the very top. Neat! Now, let’s proceed. The other method is stepping over. This means we simply execute the line of code.
So now we have progressed further down our function, and... Uh-Oh! Something has already gone wrong.
factor is only
1.0, which means what we return will just be the initial amount! That can’t be right! After a whole year, interest means that it should actaully increase, right? So what happened. Well, when we take a look at the code, we see that the
** function, or the power function, is being called with
0, and anything raised to the power of zero should in fact be
So now we have a good hypothesis that we can test: something is wrong the
time, so whatever generates that variable is also wrong. Generally, it is good practice to test your hypothesis at least once more once you make it just to make sure that you are on the right track, so let’s continue once more through he code.
So now we are back in the for loop for another iteration. Unfortunately,
balance is still
1.0. So we step into the function once more.
We are back in the function, and notice that
time is still
0. This is because the variable responsible for
year, is not being incremented. So we have our good hypothesis. Let’s test it. Go to the variable panel and click on
time. You can actually manually edit the vale within the debugger! Edit it to
1, and step over once more.
And would ya look at that! This time,
factor is no longer
1! We should continue stepping over though just to make sure that there are no other hidden problems.
Awesome. Now that we are in the for loop, we can see in the variables panel, if we continue stepping over once more, that in fact
balance does update properly now. So all that is left is for us to exit the debugger.
Simply press the red square, and you are now free to make the changes to your code that fix the issue! That’s it for using the debugger.
The final point I wish to say about debugging is attitude, which is the greatest asset. We understand that debugging is frustrating; trust me, we’ve been there. But running to a TA for help as soon as you run into a problem is not the way to approach the problem. Not only does it not help you in learning (remember that thing? It’s called the goal of the course), but it frustrates us as well that you didn’t even try and debug.
Come up with hypotheses as to what’s going wrong. This is detective work, it really is. So take a guess. Use print statements and the debugger to see if that’s the case. If you’re wrong, come up with another guess.
The time to come to a TA is when something mysterious is happening and you’ve run out of guesses. A perfect example is trying to change the value of an
int. The code seems to be fine, but it doesn’t work. Why? Well, as it turns out, you didn’t know about primitive and non-primitive types. Not your fault. So as a TA, we’d tell you about that, and using this new information, you would debug your code.
The course staff will not ever point to you and say, “Oh, the problem is in line 98.” Or “Oh, just assign
a to be
5”. Debugging your code is your exercise, and it is how you become a better programmer. Solving the problem for you may seem easier, but it deprives you of learning.