How to debug a program that is wrong
Debugging is finding out why a program does the wrong thing, and then changing it. Most of the work is not typing the fix. It is knowing which of a hundred things to look at.
Guessing and changing code at random can take all afternoon. Checking one thing at a time takes a few minutes, and this lesson shows how.
By the end of this lesson
Note
I can find a bug by reading the error and checking one thing at a time.
Key words
| Word | What it means |
|---|---|
| Bug | a fault in a program that makes it do the wrong thing |
| Reproduce | make the bug happen again, the same way, on demand |
| Traceback | the list of calls Python prints when a program stops with an error |
| Hypothesis | a guess about the cause that one check can prove wrong |
| Print check | a line that shows a value, to see what the program really holds |
| Halving | checking the middle of what is left, so each check rules out half |
The cells students fill in are left blank in this preview.
First make the bug happen on demand
You cannot fix what you cannot see. Before you change anything, find an input that makes the bug happen every time, and keep it.
Here a small program averages the sales in a file, one line a day. Its data has a gap. Make the folder and the file first.
Make a folder for the lesson
mkdir -p ~/debugtestShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Go into it
cd ~/debugtestShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the sales file
printf 'mon 120\ntue 80\nwed n/a\nthu 200\n' > sales.txtShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the report program
printf 'def parse(line):\n day, amount = line.split()\n return day, int(amount)\n\n\ndef average(lines):\n result = 0\n for line in lines:\n day, amount = parse(line)\n result = result + amount\n return result / len(lines)\n\n\nprint(average(open("sales.txt").read().splitlines()))\n' > report.pyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Run it and watch it stop
python3 report.pyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Read a traceback from the bottom up
A traceback lists the calls that were open when the program stopped. The newest call is at the bottom, so read from the bottom.
The last line says what went wrong: ValueError, with a message. The File line above it says where: a file, a line number and a function. Lines above that say how the program got there.
List only the File lines
python3 report.py 2>&1 | grep "File"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Check one thing, then the next
The error shows where the program stopped, not why the data was wrong. So make a hypothesis: one guess that one check can disprove.
Here the guess is that one line of the data is not a number. Search the file for it. If the check shows nothing, the guess was wrong, and that is also progress.
Search the data for the text
grep -n "n/a" sales.txtShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
A quick fix can hide the next bug
Line 3 says wed n/a: there was no sale figure that day. The quickest change is to count it as 0. Try it, and run the program again.
Count a missing figure as 0
sed -i 's| return day, int(amount)| if amount == "n/a":\n return day, 0\n return day, int(amount)|' report.pyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Run it again
python3 report.pyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
No error does not mean no bug
The program ran, and printed 100.0. But the average of the three days that have figures, 120, 80 and 200, is 133.3. The program is wrong and says nothing.
Now there is no traceback to read. Add a print check where the numbers are added and look at what it really holds.
Print each day as it is added
sed -i 's| result = result + amount| print(day, amount)\n result = result + amount|' report.pyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Run it and read each line
python3 report.pyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Halve the problem when there are many stages
The program has four stages. Check the middle one first. If the total is right, the fault is after it. If it is wrong, the fault is before it. Each check rules out half.
This is how a team finds which of a thousand changes broke a feature.
Worked example: checking the stages
| Stage | What it should hold | What it held | Verdict |
|---|---|---|---|
| Read the file | 4 lines | 4 lines | Fine |
| Read each amount | 120, 80, n/a, 200 | 120, 80, 0, 200 | The 0 is a made-up number |
| Add them up | 400 | 400 | Fine |
| Divide by the days counted | 400 / 3 = 133.3 | 400 / 4 = 100.0 | It counted a day with no figure |
The cells students fill in are left blank in this preview.
Write the program again, with the fix
printf 'def parse(line):\n day, amount = line.split()\n if not amount.isdigit():\n return None\n return day, int(amount)\n\n\ndef average(lines):\n result = 0\n count = 0\n for line in lines:\n pair = parse(line)\n if pair is not None:\n result = result + pair[1]\n count = count + 1\n return result / count\n\n\nprint(round(average(open("sales.txt").read().splitlines()), 1))\n' > report.pyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Run the fixed program
python3 report.pyShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
A common mistake
Warning
Many people change code before they know the cause, then change something else when that fails. After five changes nobody knows which one helped. Check first, change one thing, and run again.
Going further: finding the change that broke it
A team keeps every change in Git. When a bug appears, they know an old version was good and the newest is bad. The command git bisect asks you to test the middle version, and halves the history until one change is left.
Write the steps for 1000 changes. How many versions do you test at most?