Errors are inevitable. Python’s exception system lets you catch, handle, and recover from errors gracefully instead of crashing.

try / except / else / finally

  try:
    result = 10 / int(input("Enter a number: "))
except ZeroDivisionError:
    print("Cannot divide by zero")
except ValueError:
    print("That's not a valid number")
else:
    print(f"Result: {result}")   # runs only if no exception
finally:
    print("Done")                # always runs
  
Block When It Runs
try Always first — code that might fail
except When a matching exception occurs
else When no exception occurred
finally Always — cleanup code

Catching Multiple Exceptions

  try:
    data = json.loads(raw_text)
    value = data["key"] / data["divisor"]
except (KeyError, ZeroDivisionError) as e:
    print(f"Data error: {e}")
except json.JSONDecodeError as e:
    print(f"Invalid JSON: {e}")
  

Catching Everything (Use Sparingly)

  try:
    risky_operation()
except Exception as e:
    logger.exception("Unexpected error")
    raise  # re-raise after logging
  

Never use bare except: — it catches KeyboardInterrupt and SystemExit too.

Raising Exceptions

  def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError("Amount must be positive")
    if amount > balance:
        raise InsufficientFundsError(
            f"Cannot withdraw {amount}, balance is {balance}"
        )
    return balance - amount
  

Custom Exception Hierarchies

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

class ValidationError(AppError):
    pass

class NotFoundError(AppError):
    pass

class InsufficientFundsError(AppError):
    pass

# Catch all app errors at API boundary
try:
    process_request(data)
except AppError as e:
    return error_response(str(e), status=400)
  

Exception Chaining

Preserve the original cause when wrapping exceptions:

  try:
    config = json.loads(read_file("config.json"))
except json.JSONDecodeError as e:
    raise ConfigurationError("Invalid config file") from e
  

Common Built-In Exceptions

Exception Cause
ValueError Wrong value, right type
TypeError Wrong type for operation
KeyError Missing dict key
IndexError List index out of range
FileNotFoundError File doesn’t exist
AttributeError Missing attribute/method
ImportError Module not found
RuntimeError General runtime failure

Assertions

For conditions that should never happen in correct code:

  def calculate_discount(price, percent):
    assert 0 <= percent <= 100, "Percent must be 0-100"
    assert price >= 0, "Price cannot be negative"
    return price * (1 - percent / 100)
  

Disable assertions in production with python -O. Use exceptions for user-facing validation.

Debugging with pdb

  import pdb

def buggy_function(data):
    total = 0
    pdb.set_trace()  # breakpoint — interactive debugger starts here
    for item in data:
        total += item["value"]
    return total
  

pdb commands: n (next), s (step into), c (continue), p variable (print), q (quit).

Python 3.7+: use breakpoint() instead of pdb.set_trace().

Debugging with logging

Prefer logging over print for production debugging:

  import logging
logger = logging.getLogger(__name__)

def process(items):
    logger.debug("Processing %d items", len(items))
    for i, item in enumerate(items):
        try:
            transform(item)
        except Exception:
            logger.exception("Failed on item %d: %r", i, item)
            raise
  

Context Managers for Cleanup

Guarantee cleanup even when errors occur:

  with open("data.txt") as f:
    process(f.read())
# file closed even if process() raises

with db.transaction():
    db.execute("INSERT ...")
    db.execute("UPDATE ...")
# committed or rolled back automatically
  

Best Practices

  1. Catch specific exceptions — not bare except Exception
  2. Fail fast — validate inputs early, raise clear errors
  3. Don’t swallow exceptions — log and re-raise or handle meaningfully
  4. Use custom exceptions for domain-specific errors
  5. Clean up in finally or use context managers
  6. Write tests for error paths — they’re often untested

Robust error handling separates production code from scripts that break silently.