Keep a caller-owned stream open with nullcontext
Also published in our primary archive.
To keep a caller-owned stream open with nullcontext, wrap only the supplied stream in nullcontext(stream) and use a real context manager for a stream the helper creates. nullcontext returns its supplied object on entry and otherwise does nothing, so it does not take over closing responsibility.
The fixture makes ownership observable. process accepts either an existing StringIO or None. With provided, it chooses nullcontext(provided): the helper reads "caller", returns closed=False, and the caller can seek and read the stream again after the call. With None, the helper creates StringIO("owned") and uses that object directly as the context manager. Leaving that with block closes the helper-owned stream, so the returned flag is True.
Avoid putting an arbitrary supplied stream directly in the same with branch as a helper-created stream unless the helper’s contract says it owns both. This example is in memory, and StringIO.closed gives a direct boundary signal. It does not establish how every file-like object implements close, buffering, or ownership conventions.
contextlib.nullcontext was added in Python 3.7. The official nullcontext documentation gives the same caller-responsibility pattern for an already-open file.
AI assistance disclosure: This article was drafted with AI assistance and checked using the deterministic fixture below.
from contextlib import nullcontext
from io import StringIO
def process(stream=None):
manager = nullcontext(stream) if stream is not None else StringIO("owned")
with manager as active:
text = active.read()
return text, active.closed
provided = StringIO("caller")
assert process(provided) == ("caller", False)
assert not provided.closed
provided.seek(0)
assert provided.read() == "caller"
assert process() == ("owned", True)
print("caller-owned: caller, closed=False")
print("helper-owned: owned, closed=True")
print("caller read after call: caller")
caller-owned: caller, closed=False
helper-owned: owned, closed=True
caller read after call: caller