Legends ofPythos
Claim your name
Pythonic Python

with, and context managers

Lesson 6 of 12

Watch the lesson1:18 · with Sigrid
You have likely used open() to read or write files. If you forget to call .close(), the file handle stays open. This can lead to data loss or system errors, especially in long-running programs.

The with statement

# Manual approach (risky)
file = open('data.txt', 'w')
file.write('hello')
# If an exception happens here, close() is never called!
file.close()

# Context manager approach (safe)
with open('data.txt', 'w') as file:
    file.write('hello')
# File is closed automatically, even if an error occurs
Manual cleanup vs context manager
The with statement ensures that cleanup code runs no matter what. It works by calling the object's __enter__ method when the block starts and its __exit__ method when the block ends. If your object defines these special methods, Python treats it as a context manager.

Building custom context managers

import time
from contextlib import contextmanager

@contextmanager
def timer():
    start = time.time()
    yield  # This is where the 'with' block runs
    elapsed = time.time() - start
    print(f'Took {elapsed:.2f}s')
Using @contextmanager decorator

Handing something to the with block

from contextlib import contextmanager

@contextmanager
def notebook():
    pages = []
    yield pages      # whatever you yield lands in the `as` variable
    pages.append('filed')

with notebook() as pages:
    pages.append('first note')

print(pages)
# ['first note', 'filed']
The value after yield is what as binds.
Whatever you yield is handed to the name after as. The block then runs, and when it finishes control returns to the line straight after the yield, so anything you write there is your cleanup step.

Cleanup that still happens after an error

from contextlib import contextmanager

@contextmanager
def session(log):
    log.append('open')
    try:
        yield log
    finally:
        log.append('close')   # runs even if the block raised

log = []
try:
    with session(log):
        raise ValueError('boom')
except ValueError:
    pass

print(log)
# ['open', 'close']
Wrap the yield in try/finally so cleanup survives an exception.

Your turn

0 of 2 solved

Exercise 1

+35 XP
Write a context manager called collector using @contextmanager. It should yield an empty list. Anything added inside the with block stays in that list, and when the block ends the manager appends the string 'done' to it. Use it to collect 'a' and 'b' into a variable called box.
from contextlib import contextmanager

Run your code to check it against the tests.

Exercise 2

+35 XP
This is the real reason with exists. Write a context manager session that appends 'open' to the list log on the way in, and 'close' on the way out — even if the code inside raises an error. Then use it in a with block that raises a ValueError, catching the error outside.
from contextlib import contextmanager


log = []

Run your code to check it against the tests.