Skip to content

← Stdlib Mastery step 39 of 55

Easy Primitives

nullcontext and suppress: use the stream you were given, close only what you opened

Two small functions that between them cover the whole of contextlib‘s helper surface.

class FakeStream:
    def __init__(self, name: str, log: list[str]) -> None: ...
    def read(self) -> str: ...     # f"data from {self.name}"
    def close(self) -> None: ...   # sets self.closed, appends f"close:{name}"


def read_or_default(load: Callable[[], str], default: str) -> str: ...
def process(stream: FakeStream | None, factory: Callable[[], FakeStream]) -> str: ...
  • read_or_default returns load(), or default if the load reports the file is missing. A permission error must propagate.
  • process returns stream.read().upper(). When stream is None it opens one with factory() and closes it; when a stream was provided it must not close it.
def solve(mode: str, provided: bool) -> dict[str, object]:

solve builds a load closure that raises FileNotFoundError when mode == "missing", PermissionError when mode == "denied", and returns "payload" otherwise. It calls read_or_default(load, "DEFAULT"), catching PermissionError and recording "PermissionError" as the text. Then it calls process with FakeStream("given", log) if provided else None, and a factory producing FakeStream("opened", log). It returns:

{"text": str, "result": str, "log": [...],
 "given_closed": bool | None}     # None when no stream was provided

Why nullcontext earns its place. The alternative is an if/else with the body written twice — once for the provided stream, once for the opened one. Two copies of the same logic, which drift. nullcontext(x) yields x and does nothing on exit, so the caller’s stream survives the block while the internally-opened one is closed. That ownership rule is the real content: closing a handle you did not open is a classic way to break a caller.

Why the exception tuple must be narrow. suppress swallows the exception and abandons the rest of the block — it is not “ignore and carry on to the next statement”. And suppress(Exception) around a file read swallows PermissionError, a full disk, and every bug in the code you called, turning a misconfigured deployment into a silently empty config. The permission case here is the test for that.

Typing. nullcontext() reveals nullcontext[None] and nullcontext(5) reveals nullcontext[int] — the generic is what makes the optional-resource pattern type-check. Declare the scope as AbstractContextManager[FakeStream] so both branches of the conditional are checked against a stated interface rather than an inferred join.

FakeStream has close() but no __enter__, so it needs contextlib.closing to become a context manager.