Python's contextlib.suppress is the cleanest way to handle expected exceptions
← Back
April 4, 2026Python5 min read

Python's contextlib.suppress is the cleanest way to handle expected exceptions

Published April 4, 20265 min read

I had a codebase full of try/except/pass blocks for expected exceptions — deleting a key that might not exist, reading a file that might be missing, removing a set item that might not be there. Then I found contextlib.suppress and replaced all of them with a cleaner, more readable pattern. It signals intent clearly: "this exception is expected and acceptable here."

The basic pattern

python
from contextlib import suppress

# Before: verbose and buries the intent
try:
    del cache[key]
except KeyError:
    pass

# After: explicit and readable
with suppress(KeyError):
    del cache[key]

# Multiple exceptions
with suppress(FileNotFoundError, PermissionError):
    os.remove(temp_file)

# Works for any expected exception
with suppress(ValueError):
    value = int(user_input)  # Silently ignore if not a valid int

Real-world use cases

python
import os
import json
from contextlib import suppress
from pathlib import Path

# Cleanup operations
def cleanup_temp_files(paths: list[str]) -> None:
    for path in paths:
        with suppress(FileNotFoundError, PermissionError):
            os.remove(path)

# Dictionary operations
def remove_keys(d: dict, keys: list[str]) -> None:
    for key in keys:
        with suppress(KeyError):
            del d[key]

# Set operations
def safe_discard_all(s: set, items: list) -> None:
    for item in items:
        with suppress(KeyError):
            s.remove(item)  # discard() is better here, but just as example

# Config loading with fallback
def load_config(path: str) -> dict:
    config = {}
    with suppress(FileNotFoundError, json.JSONDecodeError):
        with open(path) as f:
            config = json.load(f)
    return config  # Returns empty dict if file missing or invalid JSON

# Registry cleanup
def unregister_handlers(registry: dict, handler_names: list[str]) -> None:
    for name in handler_names:
        with suppress(KeyError):
            del registry[name]

When NOT to use suppress

python
## WRONG: suppressing exceptions to hide bugs
with suppress(AttributeError):
    user.process_payment()  # AttributeError might mean user is None — a bug!

# RIGHT: only suppress truly expected exceptions
if user is not None:
    user.process_payment()

## WRONG: suppressing broad exceptions
with suppress(Exception):  # Way too broad — hides everything
    important_operation()

# RIGHT: be specific about which exceptions are acceptable
with suppress(ConnectionTimeout):
    cache.warm_up()

Other contextlib gems

python
from contextlib import contextmanager, asynccontextmanager, ExitStack

# @contextmanager: turn a generator into a context manager
@contextmanager
def timer(name: str):
    import time
    start = time.perf_counter()
    try:
        yield
    finally:
        elapsed = time.perf_counter() - start
        print(f"{name}: {elapsed:.3f}s")

with timer("database query"):
    results = db.execute_long_query()

# @asynccontextmanager: async version
@asynccontextmanager
async def managed_connection(pool):
    conn = await pool.acquire()
    try:
        yield conn
    finally:
        await pool.release(conn)

async with managed_connection(pool) as conn:
    await conn.execute("SELECT 1")

# ExitStack: dynamically stack context managers
def process_files(file_paths: list[str]) -> None:
    with ExitStack() as stack:
        files = [
            stack.enter_context(open(path))
            for path in file_paths
        ]
        # All files opened, all closed when block exits (even on error)
        for file in files:
            process(file.read())

suppress vs EAFP vs LBYL

Python style favors EAFP (Easier to Ask Forgiveness than Permission) over LBYL (Look Before You Leap):

python
## LBYL (check before doing) — more verbose, can race
if key in cache:
    value = cache[key]

## EAFP (try and handle) — Pythonic
try:
    value = cache[key]
except KeyError:
    value = default

## EAFP with suppress (when you don't need the value)
with suppress(KeyError):
    cache.invalidate(key)

suppress is the clearest possible signal that an exception is expected and acceptable. It is self-documenting: you can read "with suppress(FileNotFoundError)" and immediately understand that the file may not exist and that is fine. The bare try/except/pass requires reading the exception type AND inferring the intent. Use suppress whenever the intent is "this may fail and that is acceptable" — not as a catch-all for unknown failures.

Share this
← All Posts5 min read