Many Python programs open a resource and forget to close it: a file, a database connection, a lock. The @contextmanager decorator from the contextlib module turns a generator function into a context manager you use with with, so the finally block always runs and cleans up the resource — even when the code inside raises an exception. This fits any small “open — use — close” block where you do not want to write a whole __enter__/__exit__ class.

The problem

The manual way repeats the same pattern: open the resource, work inside try, close it in finally. Written by hand many times, it is long and easy to get wrong: a single branch that forgets close() leaves a database connection hanging, a file locked, or a lock never released. For a timing helper, you also have to remember to call perf_counter() at both ends.

import sqlite3
conn = sqlite3.connect("app.db")
try:
conn.execute("INSERT INTO logs (msg) VALUES (?)", ("ok",))
conn.commit()
finally:
conn.close() # forget this line and the connection hangs

The solution

With @contextmanager, the code before yield opens the resource, and the code after yield (inside finally) cleans it up. When the with block ends — normally or because of an exception — the cleanup still runs.

import sqlite3, time
from contextlib import contextmanager
@contextmanager
def get_connection(path="app.db"):
conn = sqlite3.connect(path)
try:
yield conn
finally:
conn.close()
@contextmanager
def timer(label):
start = time.perf_counter()
try:
yield
finally:
print(f"{label}: {time.perf_counter() - start:.3f}s")
with timer("write log"):
with get_connection() as conn:
conn.execute("INSERT INTO logs (msg) VALUES (?)", ("ok",))
conn.commit()

This is better because the cleanup logic is written once, reused everywhere, and anyone reading the with statement can see the resource is managed safely. One important note: a function decorated with @contextmanager must yield exactly once; yielding several times or never yielding raises an error at runtime.

Apply it to your project

Scan your codebase and turn repeated “open — use — close” patterns into dedicated context managers: one for database connections, one for timing slow tasks, one for temporarily switching the working directory or an environment variable in tests. Each time you need one, you write a single with line, the code gets shorter, and forgotten-close bugs almost disappear.

Related posts

Frequently asked questions

How is @contextmanager different from writing a class with __enter__ and __exit__?

Functionally they are equivalent for simple cases, but @contextmanager is far shorter: you write one function instead of a whole class. When the cleanup logic is complex, needs reusable state, or needs __enter__ to return several things, a class is still the clearer choice.

Does an exception inside the with block skip the cleanup?

No. The exception is thrown back into the yield point, so the finally block still runs before the exception propagates. That is exactly why this pattern is safer than calling close() by hand at the end of a function.

Can I use @contextmanager for async code?

Yes, but you must use @asynccontextmanager from contextlib with an async def function and async with. Using @contextmanager for asynchronous code will not await properly and can cause hard-to-spot bugs.

When should I not use @contextmanager?

When the context manager needs complex reusable state, or when you want precise control over __exit__ behaviour (for example, selectively swallowing exceptions). In those cases, an explicit class is easier to read and test.

💬 Have you tried this in your own project? If you hit an error or know a cleaner way, leave a comment below — I read them all and will fold the best ideas into the post.

Sources