Phase 4 · Intermediate PythonModule 20~36 min read

Errors & Exceptions

Handle failure gracefully with try/except, custom exceptions, and the EAFP style.

What you'll learn

Things go wrong: files vanish, users type nonsense, networks drop. Exceptions are Python's way of signaling and handling those failures without your whole program crashing. Handling them well is the difference between a fragile script and robust software.

By the end you'll be able to:

  • Distinguish syntax errors from exceptions and read the hierarchy
  • Use try, except, else, and finally
  • Catch specific exceptions and raise your own
  • Write in the Pythonic EAFP style

Errors & the hierarchy

A syntax error means Python can't even parse your code — it never runs. An exception happens during execution: the code is valid, but something went wrong (dividing by zero, a missing key). Exceptions form a class hierarchy — ValueError, KeyError, FileNotFoundError, and the rest all inherit from Exception — which is why you can catch them broadly or narrowly.

try / except / else / finally

The full structure has four parts, each with a clear job:

  • try — the code that might fail.
  • except — runs if a matching exception occurs.
  • else — runs only if no exception occurred.
  • finally — always runs, for cleanup, error or not.
divide.py
def safe_divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("Can't divide by zero!")
        return None
    else:
        print("Division succeeded")     # runs only if no exception
        return result
    finally:
        print("Done")                   # ALWAYS runs, error or not

print(safe_divide(10, 2))
print(safe_divide(10, 0))

Catching specific errors

Catch the specific exceptions you expect, not a blanket one. Different problems can get different handling, and you avoid accidentally swallowing bugs you didn't anticipate:

parse.py
def parse_age(text):
    try:
        return int(text)
    except ValueError:                   # bad number format
        return "Not a valid number"
    except TypeError:                    # wrong type entirely
        return "Expected a string"

print(parse_age("42"))
print(parse_age("abc"))
print(parse_age(None))

Watch out

Avoid a bare except: (or except Exception) that catches everything and hides the problem — the infamous "silent failure." Catch what you can handle; let unexpected errors surface so you can find and fix them.

raise & custom exceptions

Use raise to signal an error yourself. For domain-specific failures, define your own exception class by subclassing Exception — it makes errors self-documenting and lets callers catch exactly your case:

bank.py
class InsufficientFundsError(Exception):
    """Raised when a withdrawal exceeds the balance."""

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(f"Cannot withdraw {amount}; balance is {balance}")
    return balance - amount

try:
    withdraw(100, 250)
except InsufficientFundsError as e:
    print("Error:", e)

Tip

When re-raising inside a handler, use raise NewError(...) from original to preserve the original cause (exception chaining). The traceback then shows both errors — invaluable when debugging why something failed.

EAFP vs LBYL

Two philosophies. LBYL ("Look Before You Leap") checks conditions first; EAFP ("Easier to Ask Forgiveness than Permission") just tries the operation and handles the exception if it fails. Python leans EAFP — it's often cleaner and avoids race conditions (the thing you checked could change before you use it):

styles.py
data = {"name": "Ada"}

# LBYL — "Look Before You Leap"
if "age" in data:
    print(data["age"])
else:
    print("no age (LBYL)")

# EAFP — "Easier to Ask Forgiveness than Permission" (Pythonic)
try:
    print(data["age"])
except KeyError:
    print("no age (EAFP)")

Recap & quick check

Key takeaways

  • Syntax errors stop parsing; exceptions occur at runtime and can be caught.
  • try runs risky code; except handles failures; else runs on success; finally always runs (cleanup).
  • Catch specific exceptions — avoid bare except that hides bugs.
  • raise signals errors; subclass Exception for clear, catchable domain-specific errors; chain with 'from'.
  • Python favors EAFP (try/except) over LBYL (pre-checks) — often cleaner and race-free.

Quick check

1. Which block always runs, whether or not an exception occurred?

2. Why catch specific exceptions instead of a bare except?

3. How do you signal an error yourself?

4. How do you define a custom exception?

5. What does EAFP stand for?

Your programs handle failure gracefully now. Next we work with the outside world: Module 21 — Files, Paths & I/O.