AI Skill Report Card
Implementing Context Managers
Quick Start14 / 15
Pythonfrom contextlib import contextmanager @contextmanager def managed_resource(name): resource = acquire(name) try: yield resource finally: release(resource) with managed_resource("db") as r: use(r)
For simple cases, prefer @contextmanager over hand-written __enter__/__exit__ classes. Reach for a class-based implementation only when you need state across multiple with uses, reentrancy, or subclassing.
Recommendation▾
Add an example demonstrating ExitStack usage for dynamic/variable numbers of context managers since it's mentioned but never shown
Workflow14 / 15
Progress:
- Determine sync vs async (
withvsasync with) - Choose implementation style: generator-based (
@contextmanager) vs class-based (__enter__/__exit__) - Implement setup logic in
__enter__(or beforeyield) - Implement teardown logic in
__exit__(or afteryield, infinally) - Decide exception handling: propagate (return falsy) or suppress (return truthy)
- Handle reentrancy/reusability requirements if any
- Test normal exit, exceptional exit, and (if relevant) suppressed-exception paths
Class-based protocol
Pythonclass Resource: def __enter__(self): # setup; return value bound to `as` target return self def __exit__(self, exc_type, exc_value, traceback): # cleanup; return True to suppress exception return False
__exit__receives(None, None, None)on normal exit.- Returning a truthy value from
__exit__suppresses the exception — do this deliberately, never by accident (e.g., don't let a bareexcept:inside__exit__swallow the return value implicitly). - If cleanup itself raises, that new exception replaces the original unless you explicitly chain/reraise the original.
Generator-based protocol (contextlib.contextmanager)
Python@contextmanager def cm(): setup() try: yield value except SomeError: handle_and_suppress() # swallowing: don't re-raise finally: teardown()
- Code before
yield=__enter__; code after (infinally) =__exit__. - To suppress an exception, catch it and do not re-raise.
- To propagate, either don't catch it, or catch/log/
raise.
Async variant
Pythonclass AsyncResource: async def __aenter__(self): await self.connect() return self async def __aexit__(self, exc_type, exc_value, traceback): await self.disconnect() return False
Or with contextlib.asynccontextmanager:
Pythonfrom contextlib import asynccontextmanager @asynccontextmanager async def acm(): await setup() try: yield finally: await teardown()
Recommendation▾
Include a bad-example/good-example pair explicitly contrasting accidental exception suppression vs correct handling
Examples17 / 20
Example 1: Input: Need a context manager that times a code block and prints duration, never suppressing exceptions. Output:
Pythonimport time from contextlib import contextmanager @contextmanager def timer(label): start = time.perf_counter() try: yield finally: print(f"{label}: {time.perf_counter() - start:.4f}s")
Example 2: Input: Need a reusable, reentrant lock-like context manager as a class. Output:
Pythonclass ReentrantFlag: def __init__(self): self._depth = 0 def __enter__(self): self._depth += 1 return self def __exit__(self, exc_type, exc_value, traceback): self._depth -= 1 return False
Example 3: Input: Suppress a specific exception type using stdlib instead of custom code. Output:
Pythonfrom contextlib import suppress with suppress(FileNotFoundError): os.remove("maybe_missing.txt")
Recommendation▾
Consider a brief example showing async context manager exception propagation to complement the sync-focused examples
Best Practices
- Prefer
contextlib.suppress,contextlib.closing,contextlib.contextmanager, andcontextlib.ExitStackover reinventing them. - Use
ExitStackwhen managing a dynamic/variable number of context managers. - Always release resources in
finally(generator style) or unconditionally in__exit__(class style) — cleanup must run even if setup partially failed after acquisition. - Make
__enter__/__exit__(or the generator) side-effect-minimal and fast; put expensive logic in explicit methods called from them if reuse outsidewithis needed. - Document explicitly whether your context manager suppresses exceptions — this is a common source of silent bugs.
- For one-shot generator-based context managers, note they're not reentrant and not reusable by default (calling
with cm:twice on the same generator-based instance raisesRuntimeError). Usecontextlib.contextmanager's single-use semantics knowingly, or implement a class if reuse is required. - For async code, don't mix
__enter__/__aexit__or sync/async inconsistently — pick one protocol per class.
Common Pitfalls
- Accidentally suppressing exceptions: returning
Truefrom__exit__(or not re-raising in a caughtexceptbeforeyield) silently swallows errors. Only do this intentionally. - Cleanup masking the original exception: if teardown code itself throws, the original exception is lost. Wrap cleanup in
try/exceptand chain (raise ... from original) or log if you must swallow secondary errors. - Reusing a generator-based context manager instance across multiple
withblocks — raisesRuntimeError: generator didn't stop. Create a fresh instance each time, or use a class for reusable managers. - Forgetting
finallyin generator-based managers — an exception beforeyieldskips teardown if not guarded. - Not checking
exc_typebefore acting in__exit__— always inspect all three args if behavior depends on whether an exception occurred. - Blocking calls in
__aenter__/__aexit__— useawaitfor I/O; don't call synchronous blocking code in async context managers.