Finding a bug without guessing
The slow way to debug is to stare at the code until inspiration arrives. The fast way is to find out what the program is actually doing, which is almost never what you assumed.
prices = ["250", "120"]
total = 0
for price in prices:
total = total + int(price)
print(total)
370
That works. When it does not, the question is never "what did I write?" — it is "what value was in that variable at that moment?"
Read the traceback from the bottom
Every Python crash prints a traceback, and every beginner reads it top-down and gives up. Read the last line first.
Traceback (most recent call last):
File "shop.py", line 22, in <module>
print(receipt(order))
File "shop.py", line 15, in receipt
return format_total(total(order))
File "shop.py", line 9, in total
return sum(int(p) for p in prices)
ValueError: invalid literal for int() with base 10: 'free'
The last line is what went wrong: a ValueError, and — crucially — the
exact offending value, 'free'.
The line above it is where: shop.py line 9, inside total.
The lines above that are how you got there, most recent last. That is what "most recent call last" at the top means: the bottom of the stack is the newest frame, which is why reading upwards traces the path back to your own code.
Two lines of a traceback usually answer the whole question. The rest is context for when they do not.
print, and why repr is better
print is not a beginner's tool that real programmers outgrow. It is the
fastest way to see a value, and it is what most debugging is.
The refinement worth learning is repr:
value = "250 "
print(value)
print(repr(value))
print(len(value))
250 '250 ' 4
print shows what the value looks like. repr shows what it is, quotes
and all. A trailing space, a newline, a number that is secretly a string — all
invisible with print, all obvious with repr.
f-strings have a shorthand, !r:
name = " Ayesha\n"
print(f"name={name!r}")
name=' Ayesha\n'
Print the name alongside the value, or you will lose track of which line produced what:
subtotal = 250
tax = 37.5
print(f"{subtotal=} {tax=}")
subtotal=250 tax=37.5
The = inside the braces prints the expression and its value. It is built for
exactly this and saves typing the name twice.
Print the type when the value looks right
quantity = "3"
print(quantity)
print(f"{quantity!r} {type(quantity).__name__}")
3 '3' str
The first line looks perfectly correct and is the cause of the bug. Any time a value prints as expected but behaves oddly, print its type — it is a string where a number should be more often than any other single cause.
Bisect the problem
When you do not know where a bug is, do not read the whole program. Cut it in half.
def total(prices):
cleaned = [p.strip() for p in prices]
numbers = [int(p) for p in cleaned]
return sum(numbers)
prices = [" 250", "120 "]
cleaned = [p.strip() for p in prices]
print(f"after strip: {cleaned!r}")
numbers = [int(p) for p in cleaned]
print(f"after int: {numbers!r}")
print(total(prices))
after strip: ['250', '120'] after int: [250, 120] 370
Put a print halfway through. If the value is right there, the bug is after it; if wrong, before. Repeat, and each check halves what is left. Ten lines takes three or four checks, a thousand takes ten.
This beats reading, because reading is where the wrong assumption came from in the first place.
Check your assumption, not your code
Most bugs are a belief that does not match reality. So test the belief directly.
stock = {"chai": 12, "Coffee": 4}
wanted = "coffee"
print(wanted in stock)
print(list(stock.keys()))
print(wanted.lower() in [k.lower() for k in stock])
False ['chai', 'Coffee'] True
"The key is in the dictionary" was the assumption. One line disproved it, and the capital C is the entire bug. No amount of staring at the lookup code would have shown that, because the lookup code is correct.
breakpoint()
When printing is not enough, breakpoint() stops the program and gives you a
prompt where you can inspect anything.
def total(prices):
breakpoint()
return sum(int(p) for p in prices)
Run it from a terminal and you land in pdb. The commands worth memorising:
| Command | Does |
|---|---|
p name |
print a variable |
pp name |
pretty-print it |
n |
next line, stepping over calls |
s |
step into a call |
c |
continue to the next breakpoint |
l |
list the code around here |
w |
show the call stack |
q |
quit |
You can type any Python expression at the prompt, which is the real value — you are inside the program with all its variables live, rather than guessing from outside.
breakpoint() needs a terminal, so it does not work in the runnable blocks on
this page. Save a .py file and run it with python filename.py to try it.
Rubber-ducking
Explain the code out loud, line by line, to somebody who is not listening — a colleague, a rubber duck, an empty room. It works because speaking forces you to say what each line does rather than skimming what you meant, and the gap between the two is usually where the bug is.
More often than is comfortable, you find it mid-sentence.
Change one thing at a time
When a fix does not work, undo it before trying the next one. Three simultaneous changes that together fix the problem leave you not knowing which one mattered, and two of them are probably new bugs waiting.
Keep the program running between changes, too. A version that runs and gives a wrong answer is far more informative than one that will not start.
What to reach for, in order
- Read the last line of the traceback. It usually names the value.
- Print the suspect value with
reprand its type. Half of all bugs stop here. - Bisect — print halfway, then halve again.
- Test the assumption directly rather than the code around it.
breakpoint()when you need to look at everything at once.
And when you have found it, consider whether a check at the boundary would have caught it earlier — that is what raising errors is for, and reading errors covers the messages Python gives you along the way.
Prefer learning with a teacher?
WasiLearn Academy runs small-group classes covering this material, online and in Karachi.
See the classes