Why is a contextmanager generator required to yield exactly once?
A generator used with @contextmanager must yield exactly once because that yield separates entering the with block from leaving it. With no yield, the context manager cannot supply an entered value. With a second yield, it cannot finish its exit protocol after the block. Python raises RuntimeError in each invalid case, but at different lifecycle points.
The first fixture is still a generator because its unreachable yield is syntactically present. When with no_value() tries to enter, it raises RuntimeError; the body never runs. two_values() yields once, so its body runs and records body, but leaving the with resumes the generator to a second yield and raises RuntimeError on exit. The valid fixture records an explicit enter/body/exit sequence.
The code catches exception types instead of comparing exception messages, which are implementation details that are less useful for a portable observation. Its assertions establish the three paths for these fixtures only. They do not validate resource handling; production cleanup belongs in a finally block around the single yield.
contextlib.contextmanager has been available since Python 2.5. Its documentation describes the decorator as turning a generator function into a context-manager factory and requires the generator to yield exactly one value.
AI assistance disclosure: this article and its synthetic example were prepared with AI assistance.
Example
from contextlib import contextmanager
@contextmanager
def no_value():
if False:
yield "never"
@contextmanager
def one_value(events):
events.append("enter")
try:
yield "resource"
finally:
events.append("exit")
@contextmanager
def two_values(events):
events.append("enter")
yield "first"
events.append("between")
yield "second"
def exception_name(factory):
try:
with factory():
pass
except RuntimeError as error:
return type(error).__name__
single_events = []
with one_value(single_events) as value:
single_events.append("body")
assert value == "resource"
double_events = []
zero_result = exception_name(no_value)
two_result = exception_name(lambda: two_values(double_events))
assert zero_result == "RuntimeError"
assert two_result == "RuntimeError"
assert single_events == ["enter", "body", "exit"]
assert double_events == ["enter", "between"]
print(f"zero yield: {zero_result} at entry")
print(f"one yield: {single_events}")
print(f"two yields: {two_result} at exit")
Expected output:
zero yield: RuntimeError at entry
one yield: ['enter', 'body', 'exit']
two yields: RuntimeError at exit