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 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.
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.
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
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.
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
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
Return True when exc_type is ValueError.
Return False (or None) for any other exception type.
class Suppress:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
return exc_type is ValueError
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')
The code before yield runs on entry.
The code after yield runs on exit.
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
Append before the yield.
Pop after the yield so it runs on exit.
from contextlib import contextmanager
@contextmanager
def temp_list(lst, value):
lst.append(value)
yield
lst.pop()
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'
Insert yield between setup and teardown.
Use global mode because mode is at module level.
from contextlib import contextmanager
mode = 'idle'
@contextmanager
def active_mode():
global mode
mode = 'active'
yield
mode = 'idle'
In a class-based context manager, when does __exit__ run?
exit is guaranteed to run when the with block ends, no matter how it ends. That guarantee is the entire point of context managers.
Recap
- The
withstatement guarantees cleanup via the context manager protocol. - A class-based manager implements
__enter__and__exit__. contextlib.contextmanagerturns a generator into a manager: before yield is entry, after yield is exit.__exit__receives exception info; returnTrueto suppress, but do so rarely.- Use
withfor any resource with a defined lifetime: files, locks, connections, timers. - Multiple managers in one
withline 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.