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

  1. Reproduce the bug with the smallest input you can.
  2. Predict: for that input, what should each line do?
  3. Compare: watch what each line actually does, with a trace table or prints.
  4. The first place prediction and reality differ is your bug. Fix one thing.
  5. 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.