Debugging
Part of the free LogicWiz Python course, Module 7: Errors and debugging. The examples and practice run in the app.
Kabir's program says there are 2 toppers in the class. There are 3. No red text, no traceback, just a wrong number. This is where real programmers spend most of their time, and there is a method to it.
You'll learn to
- Debug step by step: reproduce, predict, compare, fix, re-test
- Fill a trace table by hand, the way exams ask
- Use print-debugging (and `f"{x=}"`) to see inside a program
- Explain why a bare `except:` hides bugs, and what to write instead
Learn
Debugging is a method, not luck
- Reproduce the bug with the smallest input you can.
- Predict: for that input, what should each line do?
- Compare: watch what each line actually does, with a trace table or prints.
- The first place prediction and reality differ is your bug. Fix one thing.
- Re-run every test, not just the one that failed.
Changing random lines and hoping is not debugging. Finding the first wrong value is.
Step through the toppers bug
marks = [91, 78, 95, 90]
toppers = 0
for m in marks:
if m > 90:
toppers += 1
print("Toppers (90 and above):", toppers)
It prints 2. Step through and watch toppers when m is 90: the condition is False, so 90 is not counted. The fix is m >= 90.
A trace table
A trace table is the same thing on paper: one column per variable, one row per step.
| m | m > 90 | toppers |
|---|---|---|
| 91 | True | 1 |
| 78 | False | 1 |
| 95 | True | 2 |
| 90 | False | 2 |
The last row is where reality differs from what we wanted. Exams call this a dry run. Write it neatly and you can find the output of any loop without running it.
Print-debugging
def bill(items):
total = 0
for name, price, qty in items:
line = price * qty
print("DEBUG:", name, line, total)
total = line
return total
print(bill([("samosa", 15, 2), ("chai", 10, 3)]))
It returns 30, but 2 samosas and 3 chais cost 60. The DEBUG line shows total never builds up: total = line replaces it. Change it to total += line, run, then delete the debug print.
Label your debug prints so you can find and delete them later. A shortcut: print(f"{total=}") prints total=30, the name and the value together (Python 3.8+).
Why a bare except hides bugs
except: with no error name catches everything: the KeyError you expected, but also your own typos (NameError), wrong types (TypeError), even the user pressing Ctrl+C.
So the program never crashes, but it silently does the wrong thing, and you get no traceback to tell you where. A crash with a traceback is a gift; a bare except throws it away.
Catch only the errors you expect and know how to handle. Let everything else crash loudly while you are developing.
A typo, hidden
menu = {"samosa": 15, "chai": 10}
def price_of(item):
try:
return menu[itme]
except:
return 0
print(price_of("samosa"))
It prints 0 for samosa! itme is a typo, so it raises NameError, and the bare except swallowed it. Change except: to except KeyError: and run: now the real NameError shows up and points straight at the typo.
If you really must catch broadly (say, at the very top of an app so it never shows a traceback to a customer), write except Exception as e: and print or log e. Never write except: pass.
CBSE "find the output" questions are dry runs: write a small trace table in the margin. University labs (VTU, AKTU, Anna) and vivas also ask "show the trace of this loop" and "why should you avoid a bare except?".
In one line
Debug by finding the first wrong value (trace table or labelled prints); never use a bare except:, catch only the errors you expect.