Skip to content

Error Handling and Debugging

By the end of this lesson you'll be able to:

  • Identify the three types of Python errors: syntax, runtime, and logic
  • Read a Python traceback and find exactly where the problem is
  • Use try/except to catch errors and keep your program running
  • Use raise, assert, and strategic print() calls to find and report bugs

Every programmer makes mistakes. The skill isn't avoiding errors — it's finding and fixing them quickly.

Welcome to Chapter 23!

Monty waving welcome Errors aren't failures — they're clues! Python's error messages are actually very helpful once you know how to read them. Today we'll turn scary red text into useful information. Let's code it together!

Three Types of Errors

A syntax error is caught before the program runs. Python can't parse the code at all. A missing colon after if, bad indentation, or a mismatched parenthesis causes a syntax error.

A runtime error (also called an exception) happens while the program is running. The program starts, then crashes when it hits the bad line.

A logic error is the trickiest — the program runs without crashing, but produces the wrong result. Python can't detect logic errors; you have to find them yourself.

Error type When detected Example
Syntax error Before running Missing : after if
Runtime error While running int("hello")
Logic error After running Off-by-one in a loop

Common Exception Types

When a runtime error occurs, Python raises an exception — a named error type.

Exception Cause
NameError Variable name doesn't exist
TypeError Wrong type — e.g. adding string to integer
ValueError Right type but bad value — e.g. int("hello")
IndexError List index out of range
KeyError Dictionary key doesn't exist
ZeroDivisionError Dividing by zero
AttributeError Method doesn't exist on the object
FileNotFoundError File can't be found

Reading a Traceback

When Python crashes, it prints a traceback — a trace of exactly where things went wrong.

1
2
3
4
Traceback (most recent call last):
  File "my_program.py", line 5, in <module>
    result = 10 / 0
ZeroDivisionError: division by zero

Read from the bottom up: 1. The last line gives the exception type and message: ZeroDivisionError: division by zero 2. Above it, the exact code that crashed: result = 10 / 0 3. File and line number so you know where to look: line 5

What Do You Think Will Happen?

Monty thinking The code below has a bug. Which line will crash, and what exception type will appear? Make your guess — then run it!


  

Were you right? IndexError: list index out of range — index 5 doesn't exist in a 3-item list.

try/except Blocks

A try/except block lets your program catch an error and handle it gracefully instead of crashing.

1
2
3
4
try:
    result = 10 / 0
except ZeroDivisionError:
    print("Can't divide by zero!")

Structure: 1. try: — put the risky code here 2. except ErrorType: — put the recovery code here; runs only if that error occurs


  

Catching Specific Exceptions

Catch multiple exception types with multiple except clauses:

1
2
3
4
5
6
7
8
try:
    number = int(input("Enter a number: "))
    result = 100 / number
    print(f"100 / {number} = {result}")
except ValueError:
    print("That's not a valid number!")
except ZeroDivisionError:
    print("Can't divide by zero!")

Catching specific exceptions is better than a bare except: — it prevents hiding bugs you didn't intend to catch.

else and finally Clauses

Two optional extra clauses make try/except even more powerful:

  • else: runs only if the try block completed without any exception
  • finally: runs always — whether or not an exception occurred; perfect for cleanup
1
2
3
4
5
6
7
8
9
try:
    n = int(input("Number: "))
    result = 10 / n
except (ValueError, ZeroDivisionError) as e:
    print(f"Error: {e}")
else:
    print(f"Result: {result}")   # only if no exception occurred
finally:
    print("Done.")               # always runs

The raise Statement

Use raise to trigger an exception yourself when input violates a rule:


  

The assert Statement

assert condition, "message" checks that something you believe is true actually is true. If not, it raises AssertionError. Use it for internal checks during development.

1
2
3
4
def square(n):
    result = n * n
    assert result >= 0, "Square must be non-negative!"
    return result

Validating Input Before Casting

input() always returns a string — even when you ask for a number. Casting it straight to int() crashes if the text isn't a valid number:

1
2
3
4
5
def cast_to_int():
    user_input = input("Enter an integer: ")
    return int(user_input)   # crashes with ValueError on bad input

print(cast_to_int())

Typing a word instead of a number raises ValueError: invalid literal for int() with base 10. You can catch bad input before it crashes by checking it first with the string method .isdigit(), then using assert to stop with a clear message if the check fails:

1
2
3
4
5
6
def cast_to_int():
    user_input = input("Enter an integer: ")
    assert user_input.isdigit(), "You did not enter an integer!"
    return int(user_input)

print(cast_to_int())

.isdigit() returns True only if every character in the string is a digit, so it catches bad input before int() ever runs.

Debugging with print()

The simplest debugging tool is a strategic print() statement. Insert them to see what your program is doing at each step:

1
2
3
4
5
6
7
def total_price(items):
    total = 0
    for item in items:
        print(f"  DEBUG: adding {item}")   # temporary debug print
        total += item
    print(f"  DEBUG: total = {total}")
    return total

Remove debug prints once the problem is fixed.

Using Search Engines to Debug

Monty with a tip When you see an error you don't recognize, copy the last line of the traceback and paste it into a search engine — add "Python" at the start. The Python community is enormous and someone has almost certainly had the same error before! Reading solutions on Stack Overflow is how professional programmers debug every day.

Learning Check

Your Turn — Wrap the Dangerous Code

Monty thinking The program below crashes when the user enters something that isn't a number. Wrap the int() call in a try/except ValueError block so it prints a friendly message instead!


  

Experiments

Try these changes. Predict what will happen first, then run it to check!

  1. Write code that deliberately triggers a NameError by using an undefined variable, then catch it. You'll know it worked when the error is caught and a friendly message appears instead of a crash.

  2. Write a function safe_index(lst, i) that returns lst[i] or None if i is out of range. You'll know it worked when safe_index([1,2,3], 10) returns None without crashing.

  3. Add an else clause to a try/except that prints "Success!" only when no error occurs. You'll know it worked when valid input shows "Success!" and invalid input doesn't.

  4. Add a finally clause that always prints "Calculation complete." no matter what. You'll know it worked when the message appears for both valid and invalid input.

  5. Use assert to verify that a list has exactly 3 items. Test with lists of different lengths. You'll know it worked when a 2-item list raises AssertionError and a 3-item list passes.

Debugging Champion!

Monty celebrating You can now read tracebacks, catch exceptions, raise your own errors, and debug like a professional! These skills transform error messages from obstacles into tools. That's a huge step forward. Let's keep coding!

Take the Chapter Review Quiz

See Annotated References