Skip to content

Context Managers & with Interview Questions & Answers

15 questions Updated 2026-06-18 Share:

Python interview questions on context managers and the with statement, __enter__/__exit__, contextlib.contextmanager, exception handling in __exit__, and multiple context managers.

Read the in-depth guidePython Context Managers and the with Statement Explained(opens in new tab)
15 of 15

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 ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel