AnyLearn
All lessons
Programmingbeginner

Debugging and Problem Solving: What to Do When Code Breaks

Every program breaks, so learn to fix one fast: the three kinds of error, reading a traceback from the bottom up, the beginner errors and their exact messages, print and debugger techniques for logic bugs, tests with assert, and a problem-solving routine plus a way to keep practising.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 10

Three kinds of error

Every programmer writes broken code, every day. The skill is not avoiding errors but recognising which kind you have, because each kind is found a different way:

KindWhen it shows upWhat you seeExample
Syntax errorBefore anything runsPython refuses to start, points at the lineMissing : after if
Runtime error (exception)While running, at the bad lineA traceback, then the program stopsDividing by zero
Logic errorNever, on its ownWrong output, no message at allChecking >= 50 before >= 90

Syntax errors are the easiest: Python finds them for you, before a single line runs. Runtime errors come with a detailed report of where they happened. Logic errors are the hardest, because the computer did exactly what you wrote and has nothing to complain about. Only comparing actual output with expected output reveals them.

Key idea: An error message is Python telling you precisely what it could not do and where. It is the most useful text on your screen, not an obstacle.

Full lesson text

All 10 steps on one page, for reading, reference, and search.

Show

1. Three kinds of error

Every programmer writes broken code, every day. The skill is not avoiding errors but recognising which kind you have, because each kind is found a different way:

KindWhen it shows upWhat you seeExample
Syntax errorBefore anything runsPython refuses to start, points at the lineMissing : after if
Runtime error (exception)While running, at the bad lineA traceback, then the program stopsDividing by zero
Logic errorNever, on its ownWrong output, no message at allChecking >= 50 before >= 90

Syntax errors are the easiest: Python finds them for you, before a single line runs. Runtime errors come with a detailed report of where they happened. Logic errors are the hardest, because the computer did exactly what you wrote and has nothing to complain about. Only comparing actual output with expected output reveals them.

Key idea: An error message is Python telling you precisely what it could not do and where. It is the most useful text on your screen, not an obstacle.

2. Reading a traceback from the bottom up

When a runtime error stops a program, Python prints a traceback: the chain of calls that led to the failure.

def average(scores):
    return sum(scores) / len(scores)

def report(name, scores):
    return f"{name}: {average(scores):.1f}"

print(report("Ana", [72, 88]))   # Ana: 80.0
print(report("Ben", []))
Traceback (most recent call last):
  File "report.py", line 8, in <module>
    print(report("Ben", []))
          ^^^^^^^^^^^^^^^^^
  File "report.py", line 5, in report
    return f"{name}: {average(scores):.1f}"
                      ^^^^^^^^^^^^^^^
  File "report.py", line 2, in average
    return sum(scores) / len(scores)
           ~~~~~~~~~~~~^~~~~~~~~~~~~
ZeroDivisionError: division by zero
  1. Read the last line first. The error type and message: division by zero.
  2. Then the frame just above it. Line 2, inside average, is where it happened. Since Python 3.11 the markers underneath point at the exact operation.
  3. Walk upward to find the cause. average was called by report, which was called on line 8 with an empty list. The bug is not the division; it is that nobody decided what the average of no scores should be.

3. The errors you will meet first

A handful of error types cover most beginner crashes. These are the exact messages from Python 3.12:

ErrorTypical causeMessage
SyntaxErrorMissing colon after if, for, defexpected ':'
SyntaxError= where == was meantinvalid syntax. Maybe you meant '==' or ':=' instead of '='?
IndentationErrorBlock not indented under ifexpected an indented block after 'if' statement on line 1
NameErrorTypo in a variable namename 'totl' is not defined. Did you mean: 'total'?
TypeErrorJoining text and a numbercan only concatenate str (not "int") to str
IndexErrorPosition past the end of a listlist index out of range
KeyErrorMissing dictionary key'kiwis'
ValueErrorConverting text that is not a numberinvalid literal for int() with base 10: 'abc'
ZeroDivisionErrorDividing by zero, often an empty list's lengthdivision by zero

Several of these messages suggest the fix. The "Did you mean" hints and the more specific syntax messages were added in Python 3.10, according to its release notes; older tutorials show vaguer versions.

4. Why reading the message pays off

Beginners often glance at the line number and skip the message. The research on novice programmers suggests the message itself matters.

Becker and colleagues ran a controlled study, published in Computer Science Education in 2016, with about 200 introductory students producing almost 50,000 errors. One group saw standard Java compiler messages; the other saw enhanced messages that explained the problem in plainer terms. The group with enhanced messages made fewer errors overall, fewer errors per student, and repeated the same error less often.

Python's own error messages have been rewritten along similar lines, which is why they now say things like "Did you mean: 'total'?". The practical habit:

In practice: Before touching the code, read the whole last line of the traceback out loud and say what it means in your own words: "I tried to add a string and an integer on line 12." Half the time, saying it is the fix. The other half, paste the exact message into a search engine; beginner errors are rarely new.

5. Predict the crash

Each line below fails in a different way. Name the error type before you reveal it.

Predict first

Which error does each program raise?

scores = [70, 85, 90]
print(scores[3])
total = "Score: " + 90
count = int("twelve")

6. Hunting a logic error with print

This function runs without crashing and is still wrong:

def average(scores):
    total = 0
    for i in range(1, len(scores)):
        total += scores[i]
    return total / len(scores)

print(average([80, 90, 100]))   # 63.333333333333336, expected 90

No traceback will help. Instead, make the program show you its state as it runs. Since Python 3.8, an f-string with = after a name prints the name and its value together:

    for i in range(1, len(scores)):
        print(f"{i=} {scores[i]=} {total=}")
        total += scores[i]
i=1 scores[i]=90 total=0
i=2 scores[i]=100 total=90

The output shows the loop starting at i=1: the first score, 80, is never added. range(1, len(scores)) should be range(len(scores)), or better, for s in scores. This is an off-by-one error, one of the most common logic errors there is, and exactly the kind the trace tables from the loops lesson catch.

7. Stepping through with a debugger

Print statements work, but you have to guess in advance what to print. A debugger pauses the program and lets you inspect anything. Python has one built in: put breakpoint() on a line (available since Python 3.7) and the program stops there and opens the pdb prompt.

def average(scores):
    total = 0
    for s in scores:
        breakpoint()      # execution pauses here on every pass
        total += s
    return total / len(scores)
CommandWhat it does
p totalPrint the value of an expression
nRun the next line, stepping over function calls
sStep into a function call on this line
cContinue until the next breakpoint
lList the source around the current line
qQuit the debugger and the program

Code editors such as VS Code and PyCharm wrap the same idea in a graphical interface: click in the margin to set a breakpoint, then hover over variables to see their values. The workflow is identical, so learning pdb once teaches you all of them.

8. Testing with assert

Fixing a bug is half the job. The other half is making sure it stays fixed. The simplest test in Python is assert, which does nothing when its condition is True and stops the program with an AssertionError when it is False:

def average(scores):
    if len(scores) == 0:
        return 0
    return sum(scores) / len(scores)

assert average([80, 90, 100]) == 90
assert average([50]) == 50
assert average([]) == 0
print("all tests passed")

Good test cases come from asking where code usually breaks:

  • A normal case you can check by hand: [80, 90, 100] averages 90.
  • The smallest case: one item.
  • The empty case: an empty list, an empty string, zero.
  • Boundaries: exactly 50 when the rule is >= 50, the first and last positions.
  • Every bug you have fixed, so it cannot quietly return.

Try it: Write the asserts before the function. Deciding what average([]) should return, before any code exists, is design work, and it is the decision that was missing from the traceback example.

9. From problem to working program

Plan on paper, build in small pieces, and loop back through testing whenever something fails.

flowchart TD
  A["Restate the problem in your own words"] --> B["Work 2-3 examples by hand"]
  B --> C["Split into small functions"]
  C --> D["Write asserts for one function"]
  D --> E["Write the smallest code that passes"]
  E --> F["Run the tests"]
  F --> G["Fail: read the error, trace, fix"]
  G --> F
  F --> H["Pass: next function"]
  H --> D

10. The hard part is problem solving, and how to practise it

Syntax is the visible part of programming, but it is rarely what stops people. In a study published in the ACM SIGCSE Bulletin in 2001, McCracken and colleagues set a common trial programming assessment for students finishing their first course. The combined sample of 216 students from four universities averaged 22.89 out of 110 on the study's criteria, and the authors concluded that many students do not know how to program at the end of an introductory course. Later work, including the Lister study from the loops lesson, found that many students finishing a first course also struggled to read and trace code, the step before writing it.

That is a skill built by practice, and the order of practice matters. A progression called Use, Modify, Create, described by Lee and colleagues in ACM Inroads in 2011, is a reliable way in:

  1. Use: run someone else's working program and see what it does.
  2. Modify: change it: a new rule, a new input, a different output.
  3. Create: write your own program for a problem you chose.

In practice: Take each program from this course (the tip calculator, the guessing game, the budget checker) through all three stages. Then pick a small problem from your own life, such as tracking a habit or splitting shared costs, and build it from an empty file.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. A program runs to the end with no error message but prints the wrong total. What kind of error is this?
    • Syntax error
    • Runtime error
    • Logic error
    • IndentationError
  2. Where should you start reading a Python traceback?
    • The last line, with the error type and message
    • The first line, 'Traceback (most recent call last)'
    • The line with the lowest line number
    • The frame for the main module
  3. `int("abc")` raises which error?
    • TypeError
    • NameError
    • SyntaxError
    • ValueError
  4. `for i in range(1, len(items))` skips which item of `items`?
    • The last one
    • The first one
    • None of them
    • Every second one
  5. What does `assert average([]) == 0` do if `average([])` returns 0?
    • Prints True
    • Raises AssertionError
    • Nothing, and the program continues
    • Returns 0

Related lessons