The problem with actually solves

with open(file) as f: closes the file automatically, even if the code inside the block raises an exception halfway through. That guarantee, cleanup runs no matter how the block exits, is the entire point of a context manager. Writing your own version of that guarantee by hand usually means a class with __enter__ and __exit__ methods. contextlib.contextmanager gets you the same guarantee from a single generator function.

A real, small example

Say some part of a system needs a "maintenance mode" flag flipped on for the duration of a block, and flipped back off afterward, no matter what happens inside:

from contextlib import contextmanager

state = {"maintenance_mode": False}

@contextmanager
def maintenance_mode():
    state["maintenance_mode"] = True
    try:
        yield
    finally:
        state["maintenance_mode"] = False

The yield is the entire block's body running. Everything before it is setup, everything after it (guaranteed to run via finally) is cleanup.

Confirming the guarantee actually holds

The normal case is easy to get right by hand too. The real test is whether cleanup still runs when something goes wrong:

try:
    with maintenance_mode():
        raise ValueError("something real broke")
except ValueError as e:
    print(f"caught: {e}")

print("maintenance_mode after the exception:", state["maintenance_mode"])
caught: something real broke
maintenance_mode after the exception: False

The exception propagated out normally (it's still visible to the calling code, contextmanager doesn't swallow it), and the flag still got reset. The try/finally inside the generator is what makes that true: finally runs whether the code that follows yield finishes normally or the exception unwinds straight through it. Leaving out the try/finally, writing just state["maintenance_mode"] = True; yield; state["maintenance_mode"] = False, would reset the flag on the success path and leave it stuck on True forever the moment something inside the block actually threw.

A second, narrower tool: suppress

Not every context manager is about guaranteed cleanup. contextlib.suppress exists specifically to replace a try/except/pass block that only exists to ignore one expected, specific exception type:

import os
from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove("already_gone.txt")

Functionally identical to wrapping the same line in try: ... except FileNotFoundError: pass, but it reads as what it means, "this specific failure is fine, ignore it," rather than an empty except block a reader has to infer the intent of.

The takeaway

@contextmanager turns a plain function into something with with's cleanup guarantee, using ordinary generator syntax instead of a full class. The try/finally around yield is the part doing the real work, and it's worth writing deliberately rather than assuming the decorator handles cleanup on its own. suppress covers the narrower, more common case of "ignore exactly this one exception type" without a class at all.