Debugging Methodically
Sooner or later a program runs without any error and gives the wrong answer. No traceback, no line number, just a result you know is wrong. Debugging, finding and fixing the cause, is not staring harder at the code. It is a method: make the problem happen on purpose, look at what the program actually holds, and change one thing at a time.
Think of how a good doctor works. Reproduce the symptom, run one test, change one treatment, see what happens. Prescribing everything at once might cure the patient, but nobody would know which medicine did it.
Reproduce First
Find the smallest input that shows the wrong answer, and make it happen on demand. The program below is meant to list the payments over 20. It prints four payments, including 4.20, and it leaves out 120.00, the largest of all. That is the bug, reproduced in six lines.
payments = ["54.30", "45.00", "4.20", "61.75", "12.50", "120.00"] big = [] for p in payments: if p > "20": big.append(p) print(big)
['54.30', '45.00', '4.20', '61.75']
Look at the Real Values
The code looks right. So stop reading the code you believe you wrote, and look at what the program actually has. A print at the suspect line, showing the value and its type, is the oldest debugging tool there is. Use repr in it: it shows a value the way Python would write it, quotes included, so text and numbers cannot be confused and stray spaces become visible.
payments = ["54.30", "45.00", "4.20", "61.75", "12.50", "120.00"] for p in payments[:3]: print(repr(p), type(p))
'54.30' <class 'str'> '45.00' <class 'str'> '4.20' <class 'str'>
There it is: the payments are strings. The comparison p > "20" compares text, character by character, exactly the trap from Chapter 3. The text 4.20 is greater than the text 20 because the character 4 comes after the character 2, and 120.00 is smaller because 1 comes before 2.
Pause the Program: breakpoint
For a closer look, the built-in breakpoint pauses the program at the line where you call it and opens Python's debugger, called pdb. Its prompt is the word Pdb in brackets. From there you can inspect names while the program is frozen in the middle of running. Four commands are enough to start. p followed by an expression prints it. n runs the next line. c continues to the next breakpoint. q quits.
In the session below, the debugger stops at the breakpoint line on the first round. p p shows the current payment as text, and p type(p) confirms it is a str. n moves on one line, to the comparison, and p big shows the list is still empty. Quitting asks for confirmation, because it ends the program.
payments = ["54.30", "45.00", "4.20", "61.75", "12.50", "120.00"] big = [] for p in payments: breakpoint() if p > "20": big.append(p) print(big)
> /Users/you/python-course/big_debug.py(4)<module>() -> breakpoint() (Pdb) p p '54.30' (Pdb) p type(p) <class 'str'> (Pdb) n > /Users/you/python-course/big_debug.py(5)<module>() -> if p > "20": (Pdb) p big [] (Pdb) q Quitting pdb will kill the process. Quit anyway? [y/n] y
Remember to delete the breakpoint line when you have finished; left in, it will pause the program every time.
One Change at a Time
Now fix one thing: convert each payment with Decimal before comparing, and compare with the number 20 rather than the text. Run it, and compare the result with what you expected. Had you also changed the loop and the list at the same time, and the answer came out right, you would not know which change mattered, or whether one of them broke something else.
from decimal import Decimal payments = ["54.30", "45.00", "4.20", "61.75", "12.50", "120.00"] big = [p for p in payments if Decimal(p) > 20] print(big)
['54.30', '45.00', '61.75', '120.00']
Explain It Out Loud
When you are stuck, describe the code, line by line, to someone else, or to anything that will sit still: a colleague, a friend, a rubber duck on the desk. Saying "and here it checks whether the payment is greater than twenty" is often the moment you hear that it is comparing text. Programmers call it rubber-duck debugging, and they use it far more than they admit.
- "Debugging means reading the code until I spot the mistake." Look at the running values instead. The bug is in the gap between the code you believe you wrote and what it actually does.
- "More prints are always better." A few prints at the right places, showing value and type, beat dozens scattered everywhere.
- "Changing several things at once is faster." It is faster until you cannot tell which change mattered, or which one broke something else.
- "breakpoint lines can stay in the code." Remove them when you are done, or the program will pause every time it reaches one.
- A method makes every bug finite. Without one, bugs feel like bad luck; with one, they are a list of steps.
- These habits are exactly the ones professionals use on systems a million times larger than this example.
Knowledge Check
What is the first step in debugging a wrong answer?
- Reproduce it with the smallest input that shows it
- Rewrite the function from scratch to be safe
- Add a try and except around the whole program to be safe
- Change several lines at once and see whether any of it helps
Why print repr(p) rather than just p while debugging?
- repr prints the value in a larger, bolder font in the terminal
- repr converts the value into a number just before printing it
- repr shows quotes and hidden spaces, so text is obvious
- print cannot show a value that is stored in a list
What does the debugger command n do?
- Starts a new program from the first line
- Prints the value of the name n
- Quits the debugger and ends the program
- Runs the next line, then pauses again
Why did the buggy program include "4.20" among payments over 20?
- Because 4.20 is rounded up to 20 by the comparison
- Text compares character by character, and 4 comes after 2
- Because the list was sorted into the wrong order before the loop
- Because Decimal was used in the wrong place
You got correct