Context Managers
Topic 3 of 7, with 3 concept checks. Deterministic setup and teardown with the with statement
Guarantee cleanup around a managed operation
Managed lifetimes
Pair acquisition with release through the context manager protocol, including the exceptional path, so files, locks, and temporary state cannot leak past their intended scope.
Core lesson 01
with obj: calls obj.__enter__() before the block and obj.__exit__() after — always, even on exception — making it the idiomatic way to guarantee cleanup.
Any object implementing __enter__(self) and __exit__(self, exc_type, exc_val, exc_tb) can be used with `with`. __enter__ runs at the start of the block (its return value is what `as` binds to). __exit__ runs when the block ends for any reason — normal completion or an exception — making `with` the reliable alternative to a manual try/finally for resource cleanup.
class Resource:
def __enter__(self):
print("acquiring")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("releasing")
return False # don't suppress exceptions
with Resource() as r:
print("using resource")
# acquiring / using resource / releasing -- always, in that orderWhat to remember
What does the with statement do, and what two methods make an object a context manager?
Common footguns
- Returning True from __exit__ by accident — this SUPPRESSES any exception raised in the block, rarely what you want unless deliberate.
Core lesson 02
Decorate a generator function with @contextmanager from contextlib; code before the single yield is setup, code after is teardown, and the yielded value is what `as` binds to.
Writing a full class with __enter__/__exit__ is often more ceremony than needed for simple setup/teardown pairs. contextlib.contextmanager lets you write it as a single generator function: code up to yield runs on entry, the yielded value becomes the `as` target, and code after yield runs on exit — wrapped in try/finally automatically so cleanup still happens if the block raises.
from contextlib import contextmanager
@contextmanager
def resource():
print("acquiring")
try:
yield "the-resource"
finally:
print("releasing")
with resource() as r:
print("using", r)
# acquiring / using the-resource / releasingWhat to remember
How do you write a context manager using @contextmanager instead of a class?
Common footguns
- Forgetting the try/finally around yield in your own @contextmanager function — without it, an exception in the with-block skips your cleanup code entirely.
Core lesson 03
__exit__ is always called, even on exception, and receives the exception info as arguments; returning truthy suppresses the exception, returning falsy lets it propagate.
When an exception happens inside a with block, Python calls __exit__(exc_type, exc_val, exc_tb) with the exception's details instead of (None, None, None). This lets the context manager react — log it, clean up, or suppress it entirely by returning a truthy value. If __exit__ returns anything falsy (including implicit None), the exception continues propagating after cleanup runs.
class SwallowValueErrors:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is ValueError:
print("suppressed:", exc_val)
return True # swallow it
return False # let anything else propagate
with SwallowValueErrors():
raise ValueError("oops")
print("execution continues here")What to remember
What happens to the context manager's __exit__ if an exception occurs inside the with block?
Common footguns
- Suppressing exceptions too broadly (returning True unconditionally) — this can hide real bugs just like a bare except: would.
Python lab
Browser Python lab
Runtime · idle
Python loads on your first run. Your code stays in this browser.
Best practices
- Prefer @contextmanager for simple setup/teardown pairs; use a class when you need extra state or methods.
- Always use with for files, locks, and database connections instead of manual acquire/release calls.
- Only suppress exceptions in __exit__ deliberately and narrowly — never with a bare return True.
- Nest multiple context managers in one with statement (comma-separated) instead of deeply indented nested blocks.
Apply the concept in Interview practice
Min StackmediumLeetCode #155 · O(1) per operation
Maintain a second stack tracking the running minimum alongside the main stack, so push/pop/getMin are all O(1) — the same paired-state discipline context managers rely on for enter/exit.
Open problemEncode and Decode TinyURLmediumLeetCode #535 · O(1) per operation
Maintain a dict mapping short codes to original URLs (and the reverse for encoding) — a state-lifecycle design exercise rather than a context-manager-specific one.
Open problemDesign Log Storage SystemmediumLeetCode #635 · O(n) per query
Store (id, timestamp) pairs and filter by truncating timestamps to the requested granularity — practice in resource/state lifecycle thinking similar to setup/teardown.
Open problemConcept checks
What does the with statement do, and what two methods make an object a context manager?
Hint
It guarantees cleanup code runs, even if an exception occurs inside the block.
Look for __enter__ and __exit__.
Answer
with obj: calls obj.__enter__() before the block and obj.__exit__() after — always, even on exception — making it the idiomatic way to guarantee cleanup.
Any object implementing __enter__(self) and __exit__(self, exc_type, exc_val, exc_tb) can be used with `with`. __enter__ runs at the start of the block (its return value is what `as` binds to). __exit__ runs when the block ends for any reason — normal completion or an exception — making `with` the reliable alternative to a manual try/finally for resource cleanup.
class Resource:
def __enter__(self):
print("acquiring")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("releasing")
return False # don't suppress exceptions
with Resource() as r:
print("using resource")
# acquiring / using resource / releasing -- always, in that orderWatch out
- Returning True from __exit__ by accident — this SUPPRESSES any exception raised in the block, rarely what you want unless deliberate.
How do you write a context manager using @contextmanager instead of a class?
Hint
It turns a generator function into a context manager.
Code before yield is __enter__; code after yield is __exit__.
Answer
Decorate a generator function with @contextmanager from contextlib; code before the single yield is setup, code after is teardown, and the yielded value is what `as` binds to.
Writing a full class with __enter__/__exit__ is often more ceremony than needed for simple setup/teardown pairs. contextlib.contextmanager lets you write it as a single generator function: code up to yield runs on entry, the yielded value becomes the `as` target, and code after yield runs on exit — wrapped in try/finally automatically so cleanup still happens if the block raises.
from contextlib import contextmanager
@contextmanager
def resource():
print("acquiring")
try:
yield "the-resource"
finally:
print("releasing")
with resource() as r:
print("using", r)
# acquiring / using the-resource / releasingWatch out
- Forgetting the try/finally around yield in your own @contextmanager function — without it, an exception in the with-block skips your cleanup code entirely.
What happens to the context manager's __exit__ if an exception occurs inside the with block?
Hint
__exit__ still runs — it's not skipped.
Its return value decides whether the exception keeps propagating.
Answer
__exit__ is always called, even on exception, and receives the exception info as arguments; returning truthy suppresses the exception, returning falsy lets it propagate.
When an exception happens inside a with block, Python calls __exit__(exc_type, exc_val, exc_tb) with the exception's details instead of (None, None, None). This lets the context manager react — log it, clean up, or suppress it entirely by returning a truthy value. If __exit__ returns anything falsy (including implicit None), the exception continues propagating after cleanup runs.
class SwallowValueErrors:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is ValueError:
print("suppressed:", exc_val)
return True # swallow it
return False # let anything else propagate
with SwallowValueErrors():
raise ValueError("oops")
print("execution continues here")Watch out
- Suppressing exceptions too broadly (returning True unconditionally) — this can hide real bugs just like a bare except: would.