Module 7 · Functional & Decorative Python ⏱ 19 min

Context Managers & with

By the end of this lesson you will be able to:
  • Explain why context managers guarantee cleanup code runs
  • Write a class-based context manager using __enter__ and __exit__
  • Build a simple context manager with contextlib.contextmanager

Every resource that must be released — a file, a network connection, a lock — creates a risk. If the code between open and close raises an exception, the close never runs, and the resource leaks. You could wrap everything in try...finally, but that is verbose, easy to forget, and clutters the logic you actually care about.

A context manager is Python's solution: an object that runs setup code when you enter a block and teardown code when you leave, no matter how you leave. The with statement is the syntax that uses it. You have already written with open(...) as f:; now you will learn how it works and how to write your own.

flowchart TD
  A["__enter__"] --> B["run body"]
  B --> C["no error"]
  B --> D["exception"]
  C --> E["__exit__"]
  D --> E
  style E fill:#3776ab,color:#fff
The with statement ensures __exit__ runs even if the body raises an exception.

The protocol: enter and exit

A context manager is any object with two special methods. __enter__ runs when the with block starts; its return value is bound to the variable after as. __exit__ runs when the block ends, and it receives information about any exception that occurred.

__exit__ accepts three arguments: exc_type, exc_val, and exc_tb. If the block finished normally, all three are None. If an exception was raised, they describe it. Returning True from __exit__ suppresses the exception; returning anything else lets it propagate normally. Most context managers should not suppress exceptions unless they are explicitly designed to.

A class-based context manager that prints when it enters and exits.
class EchoContext:
    def __enter__(self):
        print('Entering')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('Exiting')
        return False

with EchoContext() as ec:
    print('Inside the block')

Using contextlib.contextmanager

Writing a full class for simple setup and teardown is heavy. The contextlib module provides contextmanager, a decorator that turns a generator function into a context manager. The generator yields exactly once; everything before the yield is __enter__, and everything after is __exit__.

This style is shorter and reads linearly: setup, yield the resource, teardown. It is the preferred way to write simple context managers in modern Python, and it handles exceptions correctly without you needing to inspect exc_type manually. The decorator wraps your generator in the same __enter__ / __exit__ protocol behind the scenes.

The same behaviour written as a generator-based context manager.
from contextlib import contextmanager

@contextmanager
def echo_context():
    print('Entering')
    yield 'resource'
    print('Exiting')

with echo_context() as val:
    print('Inside, val =', val)
flowchart LR
  A["@contextmanager"] --> B["generator function"]
  B --> C["before yield = __enter__"]
  B --> D["after yield = __exit__"]
  style B fill:#3776ab,color:#fff
contextmanager converts a generator into a context manager object automatically.

A practical example: timing a block

A context manager that measures how long a block takes is useful for quick profiling. The class version stores the start time in __enter__, computes the elapsed time in __exit__, and prints a message.

The generator version does the same thing with less boilerplate. Both are correct; choose the one that matches the complexity of your setup and teardown. If you need to inspect the exception and decide whether to suppress it, the class version gives you more explicit control because you can see exc_type directly.

A timer context manager in both class and generator styles.
import time
from contextlib import contextmanager

class Timer:
    def __enter__(self):
        self.start = time.time()
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        elapsed = time.time() - self.start
        print(f'Took {elapsed:.4f}s')
        return False

@contextmanager
def timer():
    start = time.time()
    yield
    print(f'Took {time.time() - start:.4f}s')

with Timer():
    time.sleep(0.01)

with timer():
    time.sleep(0.01)

When to use what

Use with whenever you acquire a resource that must be released: files, locks, database connections, temporary directories, and timers. The with statement makes the lifetime of the resource visually obvious: it starts at the colon and ends at the dedent. A reader can see at a glance that cleanup is guaranteed.

Prefer contextmanager for simple cases where the setup and teardown are a few lines each. Use a class when you need to store complex state, expose methods on the context object, or carefully handle exceptions in __exit__. Both implement the same protocol, so callers cannot tell the difference.

Nesting context managers

You can nest with statements to manage multiple resources at once. Python also supports multiple managers in a single with line: with open('a') as f1, open('b') as f2:. The managers are entered left to right and exited right to left, which is the correct order for nested resource acquisition. This is especially important when one resource depends on another: acquire the dependency first, then the dependent resource, so the teardown order mirrors the setup order.

flowchart TD
  A["enter A"] --> B["enter B"]
  B --> C["run body"]
  C --> D["exit B"]
  D --> E["exit A"]
  style B fill:#3776ab,color:#fff
Nested context managers are entered left to right and exited right to left.
Exercise

Write a context manager class Suppress that suppresses any ValueError raised inside its block. __exit__ should return True only when the exception type is ValueError, otherwise let the exception propagate. Use is to compare types (e.g., exc_type is ValueError).

class Suppress:
    def __enter__(self):
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        # your code here
        pass
Exercise

What does this print? Trace the order of entry, body, and exit carefully.

from contextlib import contextmanager

@contextmanager
def tag(name):
    print(f'<{name}>')
    yield
    print(f'</{name}>')

with tag('div'):
    print('content')
Exercise

Write a generator-based context manager @contextmanager called temp_list that appends a given value to a list on entry and removes it on exit. The list object itself is passed as an argument. Yield nothing (or None).

from contextlib import contextmanager

@contextmanager
def temp_list(lst, value):
    # append, yield, then remove
    pass
Exercise

This context manager is meant to set a variable mode to 'active' on entry and reset it to 'idle' on exit, but it resets immediately because the yield is missing. Fix it using contextmanager and a yield.

from contextlib import contextmanager

mode = 'idle'

@contextmanager
def active_mode():
    mode = 'active'
    mode = 'idle'
Exercise

In a class-based context manager, when does __exit__ run?

Recap

  • The with statement guarantees cleanup via the context manager protocol.
  • A class-based manager implements __enter__ and __exit__.
  • contextlib.contextmanager turns a generator into a manager: before yield is entry, after yield is exit.
  • __exit__ receives exception info; return True to suppress, but do so rarely.
  • Use with for any resource with a defined lifetime: files, locks, connections, timers.
  • Multiple managers in one with line are entered left to right and exited right to left.

That completes Module 7. You can now pass functions around, process collections with map, filter, and reduce, build stateful closures, write decorators that wrap behaviour, and manage resources cleanly with context managers.

Checkpoint quiz

What does __enter__ return in a context manager?

Which is the simplest way to write a context manager for a small setup/teardown task?

Go deeper — technical resources