Course Progress64%
🍎 Python Advanced Python Topic 64 / 100
⏳ 8 min read

Context Managers

The with statement and how it guarantees setup and cleanup — no matter what happens inside the block.

"with is a promise: whatever happens inside this block, I will clean up when it exits. No matter if it succeeds or crashes."

— ShurAI

The Problem: Forgetting to Clean Up

When working with files, database connections, or network sockets, you must always clean up — close the file, release the connection. But if an error happens, you might forget:

python — risky
f = open("data.txt")
# ... if something crashes here ...
f.close()   # this line is never reached! File stays open.

A context manager guarantees cleanup through the with statement:

python — safe
with open("data.txt") as f:
    content = f.read()
    # ... even if an error happens here ...
# file is ALWAYS closed when the block exits

How with Works Under the Hood

The with statement calls two dunder methods:
__enter__
Called when entering the with block. Sets things up and optionally returns a value (the as f part).
__exit__
Called when leaving the block — whether it finished normally or raised an exception. Handles cleanup.

Building Your Own Context Manager (Class-Based)

python
class Timer:
    """Measures how long a block of code takes."""
    import time

    def __enter__(self):
        import time
        self.start = time.time()
        return self              # returned as the 'as' variable

    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        self.elapsed = time.time() - self.start
        print(f"Elapsed: {self.elapsed:.4f}s")
        return False             # False = don't suppress exceptions

with Timer() as t:
    total = sum(range(1_000_000))

# Elapsed: 0.0312s

The Easy Way — @contextmanager

Use contextlib.contextmanager to build a context manager from a generator function. The code before yield is setup; after is cleanup:

python
from contextlib import contextmanager

@contextmanager
def managed_file(filename):
    print(f"Opening {filename}")
    f = open(filename, "w")
    try:
        yield f              # ← this is the "as f" value
    finally:
        f.close()            # always runs, even on error
        print(f"Closed {filename}")

with managed_file("notes.txt") as f:
    f.write("Hello from context manager!")
# Opening notes.txt
# Closed notes.txt

Real Example — Database Transaction

python
from contextlib import contextmanager

@contextmanager
def transaction(db):
    """Commit on success, rollback on any error."""
    print("BEGIN transaction")
    try:
        yield db
        print("COMMIT")
    except Exception as e:
        print(f"ROLLBACK — {e}")
        raise

# Simulated DB object
class FakeDB:
    def insert(self, row):
        print(f"  Inserting: {row}")

db = FakeDB()
with transaction(db) as conn:
    conn.insert("User: Riya")
    conn.insert("User: Arjun")
output
BEGIN transaction
  Inserting: User: Riya
  Inserting: User: Arjun
COMMIT

"Whenever you find yourself writing try/finally just to ensure cleanup, that's a context manager waiting to happen. Extract it, name it, and reuse it anywhere."

— ShurAI

🧠 Quiz — Q1

What problem does a context manager solve?

🧠 Quiz — Q2

What does the as f part of with open("file") as f give you?

🧠 Quiz — Q3

In a @contextmanager generator, what is the role of yield?

🧠 Quiz — Q4

When does __exit__ run?