Learn AI
~40 minIn progress

Phase 1 · Programming Fundamentals

Debugging & Problem-Solving

Read tracebacks, isolate bugs with prints and checks, and break problems into clear steps.

~40 min4 sections2 exercisesQuiz · pass 4/5
On this page
  1. 01Reading a traceback
  2. 02Print debugging (still useful)
  3. 03Assert, reproduce, shrink
  4. 04A simple problem-solving loop
  5. 05Mini challenge: safe divide
  6. 06Try It
  7. 07Quiz

Reading a traceback

When Python crashes, it prints a traceback: the call stack from the failure up to your script.

Read from the bottom: the last line names the exception and message. The lines above show where it happened.

A typical error
def area(width, height):
    return width * height

# Uncommenting the next line raises TypeError:
# print(area("3", 4))

# Traceback points to the multiply line:
# TypeError: can't multiply sequence by non-int of type 'float'
# (or similar) — meaning the types were wrong.
Common exception types
# SyntaxError  — code cannot even parse (missing :, quotes)
# NameError    — variable/function name not defined
# TypeError    — wrong type for an operation
# ValueError   — right type, wrong value (int("abc"))
# IndexError   — list index out of range
# KeyError     — dict key missing
# ZeroDivisionError — division by 0
print("Learn these names—they speed up debugging.")

Fix the first real error first. Later errors often disappear once the earlier one is resolved.

Assert, reproduce, shrink

assert condition, "message" documents expectations and fails fast when they break.

Reproduce the bug with the smallest input that still fails. Smaller bugs are easier to reason about.

Assertions as guards
def discount(price, percent):
    assert price >= 0, "price must be >= 0"
    assert 0 <= percent <= 100, "percent out of range"
    return price * (1 - percent / 100)

print(discount(100, 10))
# discount(-5, 10)  # AssertionError
Reproduce with a tiny case
# Bug report: "average crashes on empty list"
# Minimal reproduce:
scores = []
try:
    avg = sum(scores) / len(scores)
except ZeroDivisionError:
    avg = 0
print(avg)

A simple problem-solving loop

1. Restate the goal in one sentence.

2. Write examples: input → expected output (including edge cases).

3. Solve the happy path with functions.

4. Add validation and error handling.

5. Test edge cases: empty, zero, huge, weird types.

From goal to tests
# Goal: clamp a number into [low, high]
# Examples:
# clamp(5, 0, 10)  -> 5
# clamp(-1, 0, 10) -> 0
# clamp(99, 0, 10) -> 10

def clamp(n, low, high):
    if n < low:
        return low
    if n > high:
        return high
    return n

tests = [(5, 0, 10, 5), (-1, 0, 10, 0), (99, 0, 10, 10)]
for n, lo, hi, expected in tests:
    got = clamp(n, lo, hi)
    print(n, "->", got, "OK" if got == expected else "FAIL")

Rubber-duck it: explain the bug out loud (or in a comment). Gaps in the story often reveal the mistake.

Practice

Mini challenge: safe divide

Write safe_divide(a, b) that returns a/b, or None if b is 0. Include two asserts for numeric-ish use (optional), then run a tiny test list of (a, b, expected) cases and print OK/FAIL.

challenge starter
def safe_divide(a, b):
    # TODO: return a/b or None when b == 0
    pass

cases = [(10, 2, 5.0), (5, 0, None), (9, 3, 3.0)]
for a, b, expected in cases:
    got = safe_divide(a, b)
    print(a, b, "->", got, "OK" if got == expected else "FAIL")

Interactive

Try It in the browser

Powered by Pyodide

Ask for two numbers as text. Try to divide float(a)/float(b). Catch ValueError and ZeroDivisionError separately and print a clear message for each.

Output

Errors

Check understanding

Lesson quiz

Score 4/5 or higher to mark this day complete.

1.Where should you usually start reading a traceback?
2.Which error means a name is not defined?
3.What is a good first debugging move?
4.What does assert do when the condition is False?
5.Why shrink a failing example?