TL;DR:
Debug in a fixed order: reproduce the bug, isolate where it starts, inspect values with print or a debugger, fix one thing, then test.
An off-by-one error comes from a loop or index that starts or stops one place early or late.
Test the fix on the failing case and on edge cases such as 0, 1 and the largest value.
Your program runs, prints an answer, and the answer is wrong. No error message, no crash. This is the hardest kind of bug, because nothing points at the line to blame. The good news is that there is a method, and it works the same way for a loop in a school assignment as for a large program.
The idea in plain words
Debugging is finding and fixing the cause of a wrong result. Guessing and changing things at random tends to waste time and add new bugs. A methodical approach treats it as a small science experiment: observe, form a guess about the cause, test the guess, and only then change the code.
The five steps
Reproduce: Find an input that fails every time. If you cannot make the bug happen on demand, you cannot know when it is fixed.
Isolate: Shrink the problem. Use the smallest input that still fails, and find the first place where reality differs from what you expected.
Inspect: Print the values that matter, or step through the code with a debugger, and compare each against what you predicted.
Fix: Change one thing that explains the difference. Do not change several things at once.
Test: Run the failing case, then the normal cases and the edges, to confirm the fix and check that you broke nothing.
Worked example: finding an off-by-one error in a loop
This function should add the whole numbers from 1 to n.
def sum_to(n):
total = 0
for i in range(1, n):
total += i
return total
print(sum_to(4))Reproduce. The program prints 6. The right answer is 1 + 2 + 3 + 4 = 10. The failure happens every time with sum_to(4).
Isolate. Try a smaller input. sum_to(1) should be 1, but this returns 0. sum_to(2) should be 3, but returns 1. The smallest failing case is sum_to(1), and the answer is short by exactly n each time: 10 − 6 = 4, 1 − 0 = 1, 3 − 1 = 2. The last number is missing.
Inspect. Add a print to see what the loop actually does.
def sum_to(n):
total = 0
for i in range(1, n):
print("adding", i)
total += i
return total
sum_to(4)It prints adding 1, adding 2, adding 3, and stops. The value 4 is never added. That is the difference between what you expected and what happened, and it points straight at range(1, n).
Fix. In Python, range(1, n) stops before n. To include n, the end value must be n + 1.
def sum_to(n):
total = 0
for i in range(1, n + 1):
total += i
return total
print(sum_to(4))Test. The failing case now prints 10. The edges: sum_to(1) gives 1, sum_to(2) gives 3, and sum_to(0) gives 0, because the range is empty. As a cross-check, the formula n(n + 1)/2 gives 4 × 5 ÷ 2 = 10. All agree.
Turn the check into a test
Once the bug is fixed, keep a record so it cannot come back. A few lines of assert do this: each one raises an error if the condition is false, and does nothing if it is true.
assert sum_to(0) == 0
assert sum_to(1) == 1
assert sum_to(4) == 10
print("all good")With the fixed function this prints all good. With the original buggy version, the second line would stop the program with an AssertionError, because sum_to(1) returned 0. A test that fails for the old code and passes for the new code is proof that the fix addressed the bug.
Why off-by-one errors are so common
Counting has two natural conventions. Humans count from 1 and include the last number. Many programming tools count from 0 and stop before the end. When you mix the two, you land one place off. The safest habit is to test the first and last item of every loop, and to say out loud which end of a range is included.
When the bug is hidden in a long program
If you do not know where the bug is, narrow it by halves. Print a value at the midpoint of the program. If it is already wrong, the bug is in the first half. If it is right, the bug is in the second half. Repeat, and you will find the line in a handful of steps. It is the same halving idea as binary search, applied to code.
How to check your answer
Can you state exactly which line caused the wrong result, and why?
Did you change only one thing between runs?
Does the failing input now pass, and do the edge cases still pass?
Did you remove the debugging prints afterwards?
Common mistakes
Changing code before understanding the bug.
Making several changes at once, so you cannot tell which one mattered.
Testing only the input that failed.
Assuming the bug is in someone else's code or in the computer before checking your own.
Skipping the small test case and going straight to the large one.
Where you will use it next
The same five steps apply to wrong SQL results, as in what a database is and how a query works, and to bugs in code that uses lists, dictionaries and sets. When the program does crash, the error message is the best clue you have.
Frequently asked questions
Should I use print or a debugger?
Print is quick for small programs. A debugger lets you pause, step and look at all the variables, which pays off as programs grow.
What is rubber duck debugging?
Explaining your code line by line to a person or an object often exposes the mistake, because you have to say what each line really does.
