Home Tutorials Error Handling

Error Handling

Exception Handling in Python: try, except, else, finally

Pyford Notes July 1, 2026 8 min read
Key points
  • Python uses exceptions for error signalling; catching them lets you recover or report gracefully.
  • Always catch specific exception types rather than bare except:.
  • The else block runs only when no exception occurred; finally always runs.
  • Define custom exception classes by inheriting from Exception.

try and except

When Python encounters an error it cannot handle automatically, it raises an exception—an object that represents the error. If the exception is not caught, it propagates up the call stack until it either reaches a handler or terminates the program with a traceback.

A try/except block intercepts the exception before that happens:

try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero")
    result = None

The code in the try block runs normally. If any line raises ZeroDivisionError, Python jumps to the matching except clause. Lines after the error inside try are skipped.

Catching specific exception types

You can handle different error types with separate clauses and access the exception object using as:

def read_config(path):
    try:
        with open(path) as fh:
            return fh.read()
    except FileNotFoundError:
        print(f"Config file not found: {path}")
        return None
    except PermissionError as exc:
        print(f"Permission denied: {exc}")
        return None

To catch multiple types in one clause, use a tuple:

except (ValueError, TypeError) as exc:
    print(f"Bad input: {exc}")

Avoid catching all exceptions with a bare except: or except Exception: at the top level unless you immediately log and re-raise. Silently swallowing unexpected exceptions is one of the most common sources of mysterious bugs.

The else and finally clauses

A try block supports two optional additional clauses:

try:
    data = json.loads(raw_text)
except json.JSONDecodeError as exc:
    print(f"Invalid JSON: {exc}")
    data = {}
else:
    # Runs only if NO exception was raised
    validate(data)
finally:
    # Always runs, exception or not
    cleanup()

else is useful when you want to run code that should only happen after a successful try, but that code itself might raise different exceptions you do not want the outer handler to catch.

finally runs regardless of what happened: success, caught exception, or even an uncaught exception bubbling past. It is the right place to release resources, close connections, or restore state.

Exception hierarchy

Python exceptions form an inheritance tree. Catching a parent class catches all its children. Key built-in types:

  • BaseException—the root. KeyboardInterrupt and SystemExit live here. Do not catch this unless you are writing a top-level runner.
  • Exception—the base for almost all catchable errors.
  • LookupError—parent of KeyError and IndexError.
  • OSError—parent of FileNotFoundError, PermissionError, ConnectionError, and many others.
  • ValueError—correct type, wrong value. TypeError—wrong type entirely.

Raising exceptions

Use raise to signal an error from your own code:

def set_age(value):
    if not isinstance(value, int):
        raise TypeError(f"Age must be int, got {type(value).__name__}")
    if value < 0 or value > 150:
        raise ValueError(f"Age {value} is outside the plausible range")
    return value

Inside an except block, a bare raise re-raises the current exception without losing the original traceback:

try:
    risky_operation()
except SomeError:
    log.error("Operation failed", exc_info=True)
    raise   # re-raises the same exception

Custom exception classes

Defining your own exception types makes error handling more precise and self-documenting:

class AppError(Exception):
    """Base class for all application-level errors."""

class ConfigError(AppError):
    """Raised when configuration is missing or invalid."""

class NetworkError(AppError):
    """Raised when a remote call fails."""
    def __init__(self, url, status_code):
        self.url = url
        self.status_code = status_code
        super().__init__(f"HTTP {status_code} from {url}")

Callers can catch the broad AppError or the specific NetworkError as needed.

Practical decision checklist

  • Catch exceptions at the level where you can actually do something useful with them.
  • Name specific exception types; a bare except catches KeyboardInterrupt too.
  • Include context in the message: which file, which URL, which value caused the problem.
  • Use finally for cleanup, not for control flow.
  • In library code, define a custom exception hierarchy so callers can choose how finely to handle errors.