Skip to content

← Stdlib Mastery step 35 of 55

Medium Primitives

contextmanager: temporarily patch an attribute, and put it back

Write a context manager that sets an attribute for the duration of a block and restores the previous state on every exit path.

class Holder:
    pass


@contextmanager
def temp_attr(obj: object, name: str, value: object) -> Iterator[None]: ...
  • Set obj.name to value on entry.
  • On exit — normal or exceptional — restore the original value, or delete the attribute if it did not exist beforehand.
  • Exceptions from the body must propagate, not be swallowed.
  • It must nest: an inner temp_attr on the same attribute restores the outer block’s value, not the original.
def solve(
    initial: dict[str, object], name: str, value: object,
    nested_value: object, raise_inside: bool, reuse: bool,
) -> dict[str, object]:

solve builds a Holder, setattrs every entry of initial onto it, then:

  1. Enters temp_attr(obj, name, value) and appends getattr(obj, name) to seen.
  2. If nested_value is not None, enters a nested temp_attr(obj, name, nested_value), appends the value seen inside, and appends the value seen again after the nested block exits.
  3. If raise_inside, raises RuntimeError inside the outer block; catches it outside and records propagated.
  4. If reuse, stores one context-manager object, enters it once successfully, then tries to enter the same object a second time and records whether that was rejected.

Returns:

{"seen": [...], "propagated": bool, "has_after": bool,
 "after": object | None, "reuse_rejected": bool}

where after is getattr(obj, name, None).

The production consequence. A generator that yields without try/finally restores state on the happy path and silently not when the body raises — which is the exact case the context manager exists for. It passes every test that does not deliberately raise inside the block, and leaves the attribute patched for the rest of the process. Use finally, not except: except forces you to re-raise and is easy to swallow by accident.

Single use. The object returned by calling a @contextmanager function wraps one generator. Entering it twice fails — on CPython 3.12-3.14 with an AttributeError, because __enter__ drops the stored constructor arguments as it runs. The exact type is an implementation detail, so record only that it was rejected. What is reusable is the function: calling it again builds a fresh generator, which is also why using it as a decorator is safe.

Typing. The return annotation is Iterator[None], not None. The generator function returns an iterator; the decorator is what turns that into a context manager. Annotating the yielded type directly is the most common mistake in the module and --strict catches it.

For “was there an attribute before?”, None is not a usable sentinel — the attribute may legitimately hold None. Use a module-level unique object.

Loading visualization…