Skip to content

Debugging Methodology, Part 1: Approach and Mental Models

← Back to Overview · Part 2: Tools and Techniques →

Learn Your Way

Read Build Watch Test Review Visualize
You are here Projects — — Flashcards —

Debugging is the systematic process of finding and fixing bugs. It is not about staring at code until you see the problem. It is a repeatable method that works on any bug, in any language. The best debuggers are not the smartest programmers; they are the most methodical.

Why This Matters

You will spend more time debugging than writing new code. A systematic approach turns frustrating hours of "why does this not work?" into a predictable process. The method described here (Reproduce, Isolate, Hypothesize, Test, Fix, Verify, Prevent) works for everything from a typo to a race condition.

The 7-step method

1. Reproduce: make the bug happen reliably

Before fixing anything, you need to see the bug yourself. Write down the exact steps:

1. Run `python app.py`
2. Enter username "alice"
3. Enter password "test123"
4. Click "View Profile"
5. ERROR: KeyError: 'email'

If you cannot reproduce the bug, you cannot verify you fixed it. Ask: "Does this happen every time? Only with certain inputs? Only on certain machines?"

2. Isolate: narrow down where the bug is

Remove variables until you find the smallest piece of code that still has the bug:

# The program is 500 lines. Where is the bug?

# Start by adding print statements at key points:
print("DEBUG: got to step 1")
print(f"DEBUG: user_data = {user_data}")
print("DEBUG: got to step 2")

# Or use the binary search method:
# 1. Add a print halfway through the code
# 2. Is the data correct at that point?
#    - YES → bug is in the second half
#    - NO  → bug is in the first half
# 3. Repeat until you find the exact line

3. Hypothesize: form a theory

Based on the error and where it occurs, make a specific guess:

  • "The email key is missing because the API response changed format"
  • "The variable is None because the database query returned no results"
  • "The loop runs one too many times because I used <= instead of <"

Do NOT start changing code randomly. Have a theory first.

4. Test: verify your hypothesis

Test your theory with the smallest possible experiment:

# Hypothesis: user_data doesn't have an 'email' key
# Test: print the actual keys
print(f"DEBUG: keys = {user_data.keys()}")

# Hypothesis: the API is returning a different format
# Test: print the raw response
print(f"DEBUG: response = {response.json()}")

If your hypothesis was wrong, go back to step 3 with new information.

5. Fix: make the minimum change

Fix only the bug. Do not refactor surrounding code, add features, or "improve" things. Keep the change as small as possible:

# Before (buggy):
email = user_data["email"]

# After (fixed):
email = user_data.get("email", "no-email@example.com")

6. Verify: confirm the fix works

Run the same reproduction steps from step 1. The bug should be gone. Also check that you did not break anything else: run the test suite.

7. Prevent: stop this bug from coming back

Write a test that catches this specific bug:

def test_missing_email_field():
    user_data = {"name": "Alice"}    # No email key
    result = process_user(user_data)
    assert result.email == "no-email@example.com"

Ask: "Why did this bug happen? Could it happen elsewhere? Is there a systematic fix?"

Debugging strategies by error type

TypeError

Usually means you passed the wrong type. Print the types of all arguments:

print(f"DEBUG: {type(x)=}, {type(y)=}")

KeyError / IndexError

Print the actual keys/length before accessing:

print(f"DEBUG: keys = {data.keys()}")
print(f"DEBUG: len = {len(my_list)}")

AttributeError

The object is not the type you think. Print what it actually is:

print(f"DEBUG: {type(obj)=}, {dir(obj)=}")

Logic errors (wrong output, no crash)

Add assertions to check your assumptions:

assert len(results) > 0, "Expected results but got empty list"
assert isinstance(user, dict), f"Expected dict, got {type(user)}"


← Overview Part 2: Tools and Techniques →