AI Skill Report Card

Implementing Context Managers

A-88·Aug 12, 2026·Source: Web
14 / 15
Python
from 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
14 / 15

Progress:

  • Determine sync vs async (with vs async with)
  • Choose implementation style: generator-based (@contextmanager) vs class-based (__enter__/__exit__)
  • Implement setup logic in __enter__ (or before yield)
  • Implement teardown logic in __exit__ (or after yield, in finally)
  • 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

Python
class 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 bare except: 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 (in finally) = __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

Python
class 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:

Python
from 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
17 / 20

Example 1: Input: Need a context manager that times a code block and prints duration, never suppressing exceptions. Output:

Python
import 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:

Python
class 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:

Python
from 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
  • Prefer contextlib.suppress, contextlib.closing, contextlib.contextmanager, and contextlib.ExitStack over reinventing them.
  • Use ExitStack when 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 outside with is 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 raises RuntimeError). Use contextlib.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.
  • Accidentally suppressing exceptions: returning True from __exit__ (or not re-raising in a caught except before yield) 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/except and chain (raise ... from original) or log if you must swallow secondary errors.
  • Reusing a generator-based context manager instance across multiple with blocks — raises RuntimeError: generator didn't stop. Create a fresh instance each time, or use a class for reusable managers.
  • Forgetting finally in generator-based managers — an exception before yield skips teardown if not guarded.
  • Not checking exc_type before acting in __exit__ — always inspect all three args if behavior depends on whether an exception occurred.
  • Blocking calls in __aenter__/__aexit__ — use await for I/O; don't call synchronous blocking code in async context managers.
0
Grade A-AI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
14/15
Workflow
14/15
Examples
17/20
Completeness
19/20
Format
15/15
Conciseness
14/15