Skip to content
datarekha
Python Easy Asked at AmazonAsked at MicrosoftAsked at Databricks

What is a context manager and how does the with statement work?

The short answer

A context manager is any object that implements __enter__ and __exit__. The with statement calls __enter__ on entry and __exit__ on exit — guaranteed, even if an exception is raised. This makes with the idiomatic way to manage resources like files, locks, and database transactions without leaking them.

How to think about it

A context manager is Python’s answer to one nagging question: how do I make sure cleanup always runs, even when things go wrong? The with statement promises that an object’s __exit__ is called no matter how the block ends — a clean finish, an early return, or an exception tearing through the middle. That promise is exactly what files, locks, and database transactions need, because each of them holds something the program must give back.

The interviewer is checking two things: can you state the protocol — __enter__ runs setup and returns the as value, __exit__ runs teardown — and do you know the contextlib.contextmanager shortcut, so you don’t write a whole class for a two-line job.

A worked example

The clearest demonstration is a transaction, because its teardown depends on how the block ended. __exit__ receives the exception (or None), so it can commit on success and roll back on failure — and it runs either way:

from contextlib import contextmanager, suppress

# Class-based: __enter__ sets up, __exit__ always tears down
class Transaction:
    def __enter__(self):
        print("BEGIN")
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("ROLLBACK" if exc_type else "COMMIT")
        return False                      # False = don't suppress the exception

with Transaction():
    print("  ... doing work ...")

# Now let an exception fly through the block — __exit__ still runs
try:
    with Transaction():
        print("  ... work that fails ...")
        raise ValueError("bad row")
except ValueError as e:
    print("caught:", e)

# Generator-based: contextlib.contextmanager. Everything before yield is setup,
# everything in the finally is teardown — far less boilerplate than a class.
@contextmanager
def managed(name):
    print(f"[{name}] acquire")
    try:
        yield name.upper()                # bound to the 'as' target
    finally:
        print(f"[{name}] release")        # runs even on exception

with managed("db_conn") as handle:
    print(f"  using {handle}")

# suppress: a tiny built-in context manager that swallows one error type
files = {"data.txt": True}
with suppress(KeyError):
    del files["missing_key"]              # no try/except needed
print("files:", files)
BEGIN
  ... doing work ...
COMMIT
BEGIN
  ... work that fails ...
ROLLBACK
caught: bad row
[db_conn] acquire
  using DB_CONN
[db_conn] release
files: {'data.txt': True}

Look at the second transaction: the block raised, yet ROLLBACK still printed before the exception continued on its way. Returning False from __exit__ is what lets that exception keep travelling — return True and you would silently swallow it. The generator version says the same thing with a try/finally around a single yield, which is why it’s what you’ll reach for most of the time.

Nested managers

Open several in one with, and they tear down in reverse order — last acquired, first released:

with open("in.txt") as src, open("out.txt", "w") as dst:
    dst.write(src.read())
Learn it properly Context Managers

Keep practising

All Python questions

Explore further