Error Handling

Master try/except, custom exceptions, EAFP vs LBYL, and context managers for robust Python code.

Beginner 30 min read 🐍 Python

try / except — Catching Errors Gracefully

Errors happen. Files don't exist, networks fail, users enter garbage data. Without error handling, these problems crash your program. Python's try/except lets you catch errors and respond gracefully instead of crashing.

Think of try/except like a safety net: you "try" something risky, and if it fails, the "except" block catches the error and handles it:

try:
    number = int(input("Enter a number: "))
    result = 10 / number
    print(f"10 / {number} = {result}")
except ValueError:
    print("That's not a valid number!")
except ZeroDivisionError:
    print("Cannot divide by zero!")
except Exception as e:
    print(f"Unexpected error: {e}")

Python checks each except clause from top to bottom and runs the first one that matches the error type. Always put more specific exceptions before general ones — if you put Exception first, it would catch everything and the specific handlers would never run.

Common exception types you'll encounter

ValueError (wrong value), TypeError (wrong type), KeyError (missing dict key), IndexError (list index out of range), FileNotFoundError, AttributeError (missing attribute), ImportError (missing module).

else and finally

The full try/except syntax includes two optional clauses: else runs only if no exception occurred, and finally runs always, whether or not an exception happened. This is perfect for cleanup:

try:
    f = open("data.txt", "r")
except FileNotFoundError:
    print("File not found!")
else:
    # Only runs if open() succeeded
    content = f.read()
    print(f"Read {len(content)} chars")
    f.close()
finally:
    # Always runs — cleanup, logging, etc.
    print("Operation complete")
Key Takeaway: else = "no errors happened", finally = "always runs". Use finally for cleanup that must happen regardless of success or failure.

Raising Exceptions

You can raise your own exceptions with the raise keyword. This is how you signal that something has gone wrong in your code — bad input, violated business rules, impossible states:

def withdraw(amount, balance):
    if amount <= 0:
        raise ValueError("Amount must be positive")
    if amount > balance:
        raise ValueError(f"Insufficient funds: need {amount}, have {balance}")
    return balance - amount

try:
    new_balance = withdraw(-50, 100)
except ValueError as e:
    print(f"Error: {e}")

try:
    new_balance = withdraw(150, 100)
except ValueError as e:
    print(f"Error: {e}")
Output
Error: Amount must be positive
Error: Insufficient funds: need 150, have 100

Custom Exceptions

For larger applications, create your own exception classes. This lets callers handle different error types differently. A common pattern is a base exception for your app, with specific subclasses:

class AppError(Exception):
    """Base exception for our application."""
    pass

class NotFoundError(AppError):
    def __init__(self, resource, resource_id):
        self.resource = resource
        self.id = resource_id
        super().__init__(f"{resource} with id {resource_id} not found")

class ValidationError(AppError):
    def __init__(self, field, message):
        self.field = field
        super().__init__(f"Validation error on '{field}': {message}")

# Usage
try:
    raise NotFoundError("User", 42)
except NotFoundError as e:
    print(f"Not found: {e}")
    print(f"Resource: {e.resource}, ID: {e.id}")
except AppError as e:
    print(f"App error: {e}")
Output
Not found: User with id 42 not found
Resource: User, ID: 42

EAFP vs LBYL

Python has two philosophies for handling potential errors:

LBYL (Look Before You Leap): Check if something will work before trying it.
EAFP (Easier to Ask Forgiveness than Permission): Just try it and handle the error if it fails.

Python strongly favors EAFP. Here's why:

# LBYL style — check first (less Pythonic)
if "key" in dictionary:
    value = dictionary["key"]
else:
    value = default_value

# EAFP style — try and catch (Pythonic)
try:
    value = dictionary["key"]
except KeyError:
    value = default_value

# Even better: use the built-in .get() method
value = dictionary.get("key", default_value)

EAFP is preferred because: (1) it avoids race conditions where the state changes between the check and the action, (2) it's often faster because the "happy path" (no error) runs without any checks, and (3) it's more readable for experienced Python developers.

⚠️ Common Mistake: Catching Too Broadly

Wrong:

try:
    do_something()
except:  # Bare except catches EVERYTHING!
    pass  # Silently swallows ALL errors

Why: Bare except catches SystemExit, KeyboardInterrupt, and every other exception — making debugging impossible. pass silently ignores the error so you never know something went wrong.

Instead:

try:
    do_something()
except Exception as e:
    logger.error(f"Failed: {e}")
    # Or: raise  — re-raise to let caller handle it
🔍 Deep Dive: Exception Chaining

Python 3 supports exception chaining with raise ... from .... When you catch one exception and raise another, you can preserve the original cause: raise AppError("processing failed") from original_error. The traceback will show both exceptions with a "The above exception was the direct cause of the following exception" message. This is invaluable for debugging complex systems.

Context Managers

The with statement ensures resources are properly cleaned up. You've seen it with files, but it works with any object that implements __enter__ and __exit__:

# Files (most common)
with open("file.txt") as f:
    data = f.read()

# Database connections
# with connect("mydb") as conn:
#     conn.execute("SELECT ...")

# Locks (threading)
# with lock:
#     shared_resource.update()

# You can use multiple context managers
with open("input.txt") as src, open("output.txt", "w") as dst:
    dst.write(src.read().upper())
Key Takeaway: Catch specific exceptions, not bare except. Use EAFP style. Create custom exceptions for your application. Always use with for resource management.