Python 3.12 ·
✓ verified by execution on 2026-07-30
Have you ever opened a file using a with statement and marvelled at how it automatically closes itself when you’re done?
The magic behind the with statement is the Context Manager Protocol. A context manager is an object that defines a runtime context by handling the entry into and exit from a block of code.
Here is a visual representation of the context management lifecycle:
The Context Manager Protocol Lifecycle
Let’s trace how execution flows from __enter__ into the block, and finally to __exit__. Change value to see how exceptions trigger early exit!
python · visualize
class SuppressErrors: def __enter__(self): print("Entering context") return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is not None: print(f"Exception suppressed: {exc_type.__name__}") return True # Suppress the exception! print("Exiting normally")value = 0with SuppressErrors(): print(f"Working with value: {value}") if value == 0: raise ValueError("Cannot be zero!") print("Work complete")print("Execution continues safely")
Building Class-Based Context Managers
To build a custom context manager using a class, you just need to implement __enter__ and __exit__.
python
class Timer: def __enter__(self): print('Timer started') return self def __exit__(self, exc_type, exc_val, exc_tb): print('Timer stopped')with Timer() as t: print('Doing work')
Output
Timer started
Doing work
Timer stopped
Your output
Suppressing Exceptions
The __exit__ method takes three arguments related to an exception: the exception type, value, and traceback. If the context was exited normally, all three are None.
If an exception occurs and __exit__ returns a truthy value, Python suppresses the exception!
python
class SuppressErrors: def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is not None: print(f'Suppressed {exc_type.__name__}') return Truewith SuppressErrors(): raise ValueError('Oops')print('Execution continues')
Output
Suppressed ValueError
Execution continues
Your output
The @contextmanager Decorator
Writing classes just to get __enter__ and __exit__ can be boilerplate-heavy. Python’s contextlib module provides the @contextmanager decorator, which turns a generator function into a full context manager.
Everything before the yield statement acts as __enter__. The yield itself passes the bound value to the as clause. Everything after the yield acts as __exit__.
When using @contextmanager, any unhandled exception in the with block is literally re-raised inside your generator at the line where yield occurred. To handle cleanup safely, you must wrap your yield in a try/finally block!
Sometimes you don’t know ahead of time how many context managers you need to open (e.g., dynamically opening multiple files). The ExitStack is designed exactly for this: it allows you to dynamically register context managers, and it ensures all of them are cleaned up properly when the stack is exited.
python
from contextlib import ExitStackwith ExitStack() as stack: print('Managing multiple contexts dynamically') # stack.enter_context(open('file.txt')) would go here print('Done')
Output
Managing multiple contexts dynamically
Done
Your output
The Standard Library Example
The most prominent example of a context manager is file handling. File objects natively implement this protocol.
python
import tempfileimport oswith tempfile.NamedTemporaryFile(delete=False) as f: f.write(b'Hello') name = f.namewith open(name, 'r') as f: print(f.read())os.remove(name)
Output
Hello
Your output
Check yourself
In `with open(path) as f:`, what exactly gets bound to f?
Reveal answer
Whatever __enter__ returns — The as-name receives the return value of __enter__. That is often self, but it does not have to be.
An exception is raised inside a with block. Does __exit__ still run?
Reveal answer
Yes — on success, on exception, and on early return — Guaranteed cleanup is the entire point of a context manager: __exit__ runs on every exit path.
In an @contextmanager generator, where does an exception from the with body surface?
Reveal answer
At the yield statement, so teardown needs try/finally around it — The exception is thrown in at the yield. Without try/except/finally around it, your teardown code never runs.
What does returning a falsey value from __exit__ do?
Reveal answer
Lets the original exception propagate normally — Return truthy to suppress the exception; falsey (including None) lets it propagate — which is what you almost always want.
Challenges
Challenge 1 +100 XP
Create a class `ExecutionTimer` that implements the context manager protocol. It should record `self.start = time.time()` in `__enter__` and `self.end = time.time()` in `__exit__`. It should also calculate `self.duration` in `__exit__`.
python
Test 1 — expects "True\n"
Need a hint? (−25% XP)
Remember to return `self` from `__enter__` so the object can be accessed if used with `as`.
Use `@contextmanager` to write a context manager called `temp_setattr` that temporarily sets an attribute on an object, and restores the original value when exiting.
python
Test 1 — expects "2\n1\n"
Need a hint? (−25% XP)
Use a try/finally block around yield to guarantee the attribute is restored even if an exception occurs.
Show solution (0 XP)
from contextlib import contextmanager@contextmanagerdef temp_setattr(obj, attr, value): original = getattr(obj, attr) setattr(obj, attr, value) try: yield finally: setattr(obj, attr, original)class Dummy: val = 1d = Dummy()with temp_setattr(d, "val", 2): print(d.val)print(d.val)