A context manager is an object that defines setup and teardown logic to run
around a block of code. The with statement uses it to guarantee that the
teardown happens — even if the block raises an exception or returns early — so you
don't have to write manual try/finally.
with open("data.txt") as f: # __enter__ runs, returns the file
data = f.read()
# __exit__ runs here automatically — the file is closed
The classic use is resource management: files, network sockets, database
connections, and locks. Rule of thumb: any "acquire then must-release" pattern is
a candidate for a with block.
To make a class a context manager you implement two dunder methods. __enter__
runs at the start of the with block and its return value is bound to the as
variable. __exit__ runs when the block ends — always — receiving the
exception type, value, and traceback (all None if the block succeeded).
class Timer:
def __enter__(self):
import time; self.start = time.time()
return self # bound to 'as t'
def __exit__(self, exc_type, exc_val, exc_tb):
import time; print(time.time() - self.start)
return False # don't suppress exceptions
with Timer() as t:
do_work()
__exit__ always runs, which is what makes cleanup reliable. Rule of thumb:
acquire the resource in __enter__, release it in __exit__, and return self
if callers need the object.
The @contextlib.contextmanager decorator lets you write a context manager as a
generator instead of a class. Code before yield is the setup (__enter__),
the yielded value becomes the as target, and code after yield is the
teardown (__exit__).
from contextlib import contextmanager
@contextmanager
def opened(path):
f = open(path) # setup
try:
yield f # value bound to 'as'
finally:
f.close() # teardown — runs even on error
with opened("data.txt") as f:
print(f.read())
The try/finally around the yield is essential — without it the teardown is
skipped when the block raises. Rule of thumb: reach for the decorator for simple,
one-off managers; write a class when you need state across multiple methods.
When the with block raises, Python passes the exception details into __exit__.
The crucial part is the return value: returning a truthy value tells Python
to suppress the exception, while returning False/None lets it propagate
normally.
class Suppress:
def __enter__(self): return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is ValueError:
print("swallowed:", exc_val)
return True # suppress ValueError
return False # re-raise anything else
with Suppress():
raise ValueError("oops") # swallowed, program continues
contextlib.suppress(ValueError) is the ready-made version of this pattern. Rule
of thumb: only suppress exceptions you genuinely intend to ignore — accidentally
returning a truthy value hides bugs.
You can manage several resources in one with by separating them with commas (or
using parenthesized form in 3.10+). They are entered left to right and
exited in reverse order, so cleanup unwinds correctly.
with open("in.txt") as src, open("out.txt", "w") as dst:
dst.write(src.read())
# dst closed first, then src
import threading
lock = threading.Lock()
with lock: # acquire on enter, release on exit
shared_counter += 1
Common real uses: files (auto-close), locks (auto-release even on error),
database transactions (commit/rollback), and temporarily changing state
like decimal.localcontext. Rule of thumb: if you ever write try/finally to
release something, a context manager expresses it more clearly.
__exit__(exc_type, exc_value, tb) receives the exception (or three Nones
on clean exit). Returning a truthy value swallows the exception; returning
None/falsy lets it propagate. Suppressing should be deliberate — silently
eating errors hides bugs.
class Ignore:
def __enter__(self): return self
def __exit__(self, et, ev, tb):
return et is ValueError # swallow only ValueError
with Ignore():
raise ValueError("gone") # suppressed
Rule of thumb: return True from __exit__ only when you intentionally mean
to suppress that specific exception.
contextlib.suppress(*exc_types) is a context manager that ignores
the listed exceptions inside its block — a clean replacement for a
try/except: pass. It only swallows the named types; others propagate.
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("maybe.txt") # no error if it's missing
# equivalent to:
try: os.remove("maybe.txt")
except FileNotFoundError: pass
Rule of thumb: use suppress for the narrow "ignore this expected error"
case instead of an empty-except block.
closing(thing) turns any object with a .close() method into a context
manager that calls close() on exit — useful for objects that don't natively
support with. It guarantees cleanup even on exceptions.
from contextlib import closing
from urllib.request import urlopen
with closing(urlopen(url)) as page:
data = page.read() # page.close() runs automatically
Rule of thumb: wrap legacy "has close() but no with" objects in closing()
to get deterministic teardown.
The code before yield is setup, the value yielded is bound to as,
and code after yield is teardown. If the with body raises, the
exception is thrown into the generator at the yield, so cleanup runs
only if you put it in a finally.
from contextlib import contextmanager
@contextmanager
def transaction(db):
db.begin()
try:
yield db
db.commit()
finally:
db.close() # always runs, even on error
Rule of thumb: in a @contextmanager, always guard teardown with
try/finally so it survives exceptions in the body.
ExitStack manages a dynamic/variable number of context managers —
when you don't know how many you'll open (e.g. a list of files). You
enter_context() each, and all are unwound in reverse order when the stack
exits, even on error.
from contextlib import ExitStack
with ExitStack() as stack:
files = [stack.enter_context(open(p)) for p in paths]
# all files closed automatically at block end
Rule of thumb: use ExitStack when the set of resources is computed at
runtime rather than known statically.
It depends on the implementation. A @contextmanager generator is
single-use — its generator is exhausted after one with, so reusing it
raises RuntimeError. A class-based manager can be reusable (or even
reentrant) if you design __enter__/__exit__ to support it, like
threading.Lock.
from contextlib import contextmanager
@contextmanager
def cm(): yield
c = cm()
with c: ...
with c: ... # RuntimeError: generator already executed
Rule of thumb: assume @contextmanager objects are one-shot; create a fresh
one per with unless the manager explicitly supports reuse.
It's whatever __enter__ returns. It is not necessarily the context
manager itself — e.g. open() returns the file object, while a lock's
__enter__ often returns None. Forgetting to return self/the resource is
a common bug.
class Conn:
def __enter__(self):
self.handle = connect()
return self.handle # this is what `as` binds
def __exit__(self, *a):
self.handle.close()
with Conn() as h: # h is the handle, not Conn
h.query(...)
Rule of thumb: design __enter__ to return the object the user actually needs
inside the block.
One implementing __aenter__/__aexit__ (coroutines), driven by async with. It's for setup/teardown that involves awaiting — opening an async DB
connection or HTTP session. contextlib.asynccontextmanager provides the
generator form.
from contextlib import asynccontextmanager
@asynccontextmanager
async def session():
s = await open_session()
try:
yield s
finally:
await s.close()
async with session() as s: ...
Rule of thumb: use async context managers whenever acquiring or releasing the
resource itself requires await.
Separate them with commas. They enter left to right and exit right to
left. Python 3.10+ allows parenthesized multi-line groups for
readability. This avoids deep nesting of with blocks.
with open("in.txt") as fin, open("out.txt", "w") as fout:
fout.write(fin.read())
# 3.10+ parenthesized form:
with (
open("a") as a,
open("b") as b,
):
...
Rule of thumb: combine related managers in one with; for a runtime-sized
list, use ExitStack instead.
When the with body raises, the exception is re-raised at the yield
inside the generator. You can try/except around the yield to react; to
suppress it, simply catch it and don't re-raise (the generator returning
normally suppresses). Re-raise (or don't catch) to let it propagate.
from contextlib import contextmanager
@contextmanager
def ignore_value_error():
try:
yield
except ValueError:
pass # suppresses ValueError from the body
with ignore_value_error():
raise ValueError("gone") # swallowed
Rule of thumb: catch at the yield to observe or suppress body exceptions;
let them propagate by not catching.
More Errors & Exceptions interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.