Lesson 117 min read

Error Handling

When things go wrong — and they will — be ready

Why Errors Happen (And That's OK)

Errors are a normal part of programming — like potholes on a road. The question isn't "will I hit one?" but "am I ready when I do?" Python's error handling lets you catch problems gracefully instead of your program crashing and burning.

Python has two kinds of problems:

  • Syntax errors — You typed something Python can't understand. Like a typo in a recipe: prnt("hello"). Python catches these before running.
  • Exceptions — Your code is valid Python, but something went wrong at runtime: dividing by zero, opening a file that doesn't exist, accessing a list index that's out of range. When an exception is thrown, Python walks up the call stack looking for a handler.

Common Exceptions

# Let's see some common exceptions (don't worry, we'll catch them!)
examples = [
("ZeroDivisionError", lambda: 10 / 0),
("IndexError", lambda: [1, 2, 3][99]),
("KeyError", lambda: {"a": 1}["z"]),
("TypeError", lambda: "hello" + 5),
("ValueError", lambda: int("abc")),
("FileNotFoundError", lambda: open("nope.txt")),
]
for name, func in examples:
try:
func()
except Exception as e:
print(f"{name}: {e}")
Output
ZeroDivisionError: division by zero
IndexError: list index out of range
KeyError: 'z'
TypeError: can only concatenate str (not "int") to str
ValueError: invalid literal for int() with base 10: 'abc'
FileNotFoundError: [Errno 2] No such file or directory: 'nope.txt'

Try / Except / Else / Finally

The try/except block is your safety net. Here's how the four parts work:

  • try — Put the risky code here. "Try this, and if it blows up..."
  • except — "...catch the explosion and handle it." You can catch specific exception types or all of them.
  • else — Runs ONLY if try succeeded with no errors. "If everything went well, do this too."
  • finally — Runs NO MATTER WHAT — error or not. Perfect for cleanup. "Always do this when we're done."
try/except/else/finally — else runs only on success, finally runs no matter what

The Full Try/Except Pattern

def safe_divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Can't divide by zero!")
return None
except TypeError:
print("Both arguments must be numbers!")
return None
else:
print(f"{a} / {b} = {result}") # Only runs if no error
return result
finally:
print("--- Division attempt complete ---") # Always runs
safe_divide(10, 3)
print()
safe_divide(10, 0)
print()
safe_divide(10, "two")
Output
10 / 3 = 3.3333333333333335
--- Division attempt complete ---

Can't divide by zero!
--- Division attempt complete ---

Both arguments must be numbers!
--- Division attempt complete ---

Raising Exceptions

Sometimes you want to raise an error on purpose. Maybe someone passed invalid data to your function, and you want to stop things early with a clear message. Use raise for this.

Raising & Custom Exceptions

# Raising built-in exceptions
def set_age(age):
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0 or age > 150:
raise ValueError(f"Age must be between 0 and 150, got {age}")
print(f"Age set to {age}")
set_age(25)
try:
set_age(-5)
except ValueError as e:
print(f"Caught: {e}")
print()
# Custom exception classes
class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(
f"Can't withdraw ${amount}. Balance is only ${balance}."
)
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
try:
new_balance = withdraw(50, 75)
except InsufficientFundsError as e:
print(e)
print(f"You're short by ${e.amount - e.balance}")
Output
Age set to 25
Caught: Age must be between 0 and 150, got -5

Can't withdraw $75. Balance is only $50.
You're short by $25

Best Practices

Here are the golden rules of error handling:

  • Be specific — Catch ValueError, not bare except:. A bare except catches everything, including keyboard interrupts and system exits. That's almost never what you want.
  • Don't silence errors — Never write except: pass. If something goes wrong, you want to know about it.
  • Use else — Put success-only code in else, not in try. This way you don't accidentally catch errors from the success code.
  • EAFP over LBYL — Python prefers "Easier to Ask Forgiveness than Permission." Try the operation and catch the error, rather than checking every possible condition first.

EAFP vs. LBYL

data = {"name": "Alice", "scores": [85, 92, 78]}
# LBYL — Look Before You Leap (less Pythonic)
if "scores" in data and len(data["scores"]) > 0:
avg = sum(data["scores"]) / len(data["scores"])
print(f"LBYL average: {avg}")
# EAFP — Easier to Ask Forgiveness (more Pythonic)
try:
avg = sum(data["scores"]) / len(data["scores"])
print(f"EAFP average: {avg}")
except (KeyError, ZeroDivisionError) as e:
print(f"Couldn't calculate: {e}")
Output
LBYL average: 85.0
EAFP average: 85.0
Note: Think of try/except like a stunt double in a movie. You let the stunt double (try block) do the dangerous stuff, and if something goes wrong, the safety crew (except block) handles it — so the movie (your program) keeps rolling.

Quick check

When does the else block run in a try/except/else?

Continue reading

StackData Structure
LIFO — array & linked-list backed
Error HandlingJavascript
try/catch and throwing your own errors
Exception HandlingJava
try/catch, checked vs unchecked, and throws
File I/OClasses & OOP