post · Debugging · Skills

Debug Like a Working Engineer, Not a Panicked Student

Most students debug by adding print statements at random and praying. Working engineers don't, because we've spent enough hours on a single bug to know that praying is slow. The method below is what I use on real code at work and on every assignment that lands in my inbox.

It works in any language. It isn't glamorous. It works.

Step 1, Read the error message

Sounds obvious. Roughly half the time when a student sends me code that "doesn't work", the error message tells you exactly what is wrong, in plain English, with a line number.

Traceback (most recent call last):
  File "lab3.py", line 14, in <module>
    print(students[5])
IndexError: list index out of range

The error says: line 14, list index out of range. Look at line 14. Look at students. Count its elements. The list has fewer than 6 elements. You tried to access index 5. Done.

Students will read past this and start changing unrelated code. Don't. Read the error fully, twice.

Step 2, Form a hypothesis

Before you change anything, write down (on paper, in a comment, in your head) exactly what you think is happening and what you expect to happen instead.

Bad: "It isn't working." Good: "I expect the loop to run 5 times because the list has 5 items. The print statement runs 6 times. So the loop is running one extra iteration. Off-by-one error somewhere in the bounds."

This step is what separates fast debugging from slow debugging. If you can't articulate what is broken, your changes are random and won't converge on a fix.

Step 3, Reduce until you have a minimal reproduction

This is the single highest-leverage debugging skill. Take the code that fails and start cutting it down. Remove everything that isn't strictly required to reproduce the bug. Keep cutting until you've the smallest possible program that still misbehaves.

You do this for two reasons:

  • Most bugs become obvious when the surrounding code is gone.
  • If they aren't obvious, you now have a tiny reproduction you can show to a tutor, a friend, or me.

Example. Your 200-line assignment is throwing a KeyError. Cut everything except the dictionary creation and the lookup. If the error still happens, the bug is in those few lines. If the error goes away, add code back in chunks until it returns.

This is called bisection. Engineers use it daily.

Step 4, Use the debugger, not print statements

Print statements are the slowest debugging tool we have. Every time you change one, you save the file, run the program, and read the output. That's slow.

In Python use breakpoint() (Python 3.7+):

def calculate(items):
    total = 0
    breakpoint()      # execution stops here, you get an interactive prompt
    for x in items:
        total += x
    return total

When you run the program, you can inspect any variable, step through line by line (n), step into functions (s), and continue (c). It takes 10 minutes to learn and saves you hours forever.

In Java, use IntelliJ's debugger, set a breakpoint, run in Debug mode. In JavaScript, use Chrome DevTools. Every editor has one. Use it.

If you absolutely must use prints, log the types and values of your variables, not just messages:

print(f"DEBUG students={students!r} type={type(students)} len={len(students)}")

!r calls repr(), which shows you the structure including quotes around strings. type() reveals the actual type. This is much more informative than print(students).

A concrete example

Student sends me this Python code, says it crashes:

def average_grades(students):
    total = 0
    for s in students:
        total += s["grade"]
    return total / len(students)

result = average_grades([{"name": "A", "grade": 80}, {"name": "B", "grade": 90}])

Student reports: KeyError: 'grade'.

Step 1, read the error: KeyError on key 'grade'. So somewhere in the dict, 'grade' is missing.

Step 2, hypothesis: maybe one of the dicts in the list doesn't have 'grade'.

Step 3, reduce: comment out the loop, run again. No error. So it is in the loop. Add print(s) before the access. Run.

Output:

{'name': 'A', 'grade': 80}
{'name': 'B', 'grade': 90}

Both dicts have 'grade'. So the error must come from a different call. Search the file. Find another call earlier:

result = average_grades([{"name": "C"}])

There it is. One of the test cases passes a dict without a 'grade' key. Two minutes of focused debugging instead of 30 minutes of guessing.

Bugs you'll see again and again

After enough debugging sessions, you start recognising patterns. The most common in student code:

  • Off-by-one errors: you iterate from 0 to len(list) inclusive instead of exclusive, or you use <= where you meant <.
  • Type confusion: a string when you expected an int, or vice versa. Print type(x) early.
  • Mutability surprises: see the Python mistakes post.
  • Scope problems: the variable you think is global is actually shadowed inside a function.
  • Reference vs value: passing a list into a function and the function modifies it. The caller sees the change.

When you've seen each of these once and understood why, the second time becomes 10 seconds of recognition.

When to stop debugging and ask for help

There is a healthy ratio of struggle to help. Roughly:

  • 0 to 30 minutes stuck: keep debugging. You learn most here.
  • 30 minutes to 2 hours: take a break, come back fresh. Read your code out loud.
  • More than 2 hours on the same bug: ask. You're not getting smarter, just tired.

When you do ask, send a minimal reproduction, the error message, and what you've already tried. That triples the chance of getting a useful answer fast.

If you have a bug you've stared at for too long, send me the code on Telegram. I find the bug, explain why it happened, and show you the pattern to recognise it next time.

Stuck on something specific?

Send your brief and I will reply with a fixed price, usually within the hour.