Avoid catching GeneratorExit in library code
Also published in our primary archive.
Avoid catching GeneratorExit in library code
To avoid catching GeneratorExit in library generator code, do not use a broad except BaseException around a yield. Generator closing injects GeneratorExit at the suspended yield point. A broad handler can intercept it, and if the generator then yields again it violates the generator-close protocol, causing close() to raise RuntimeError.
The broken_stream fixture demonstrates that failure. After yielding 1, it catches every BaseException, records GeneratorExit, and attempts to yield 2; closing it therefore reports RuntimeError. The corrected safe_stream has no broad exception handler around its yield. Its finally block owns cleanup, so calling close() records safe:cleanup and terminates the generator. The final assertion verifies that another next() raises StopIteration after close.
A normal except Exception does not catch GeneratorExit, but the most precise design is to catch only application exceptions that the generator can actually recover from. Do not suppress GeneratorExit just to log it. If cleanup itself can fail, consider how that failure should be surfaced, because it can affect generator shutdown.
Generator.close() and its handling of GeneratorExit are specified by Python's yield-expression reference; the behavior shown applies to supported Python 3 versions. See yield expressions.
AI assistance disclosure: This article was drafted with AI assistance and checked using the synthetic example shown below.
events = []
def broken_stream():
try:
yield 1
except BaseException as exc:
events.append(f"broken:caught:{type(exc).__name__}")
yield 2
def safe_stream():
try:
yield 1
finally:
events.append("safe:cleanup")
broken = broken_stream()
print(f"broken-first:{next(broken)}")
try:
broken.close()
except RuntimeError:
print("broken-close:RuntimeError")
safe = safe_stream()
print(f"safe-first:{next(safe)}")
safe.close()
try:
next(safe)
except StopIteration:
print("safe-after-close:StopIteration")
assert events == ["broken:caught:GeneratorExit", "safe:cleanup"]
for event in events:
print(event)
broken-first:1
broken-close:RuntimeError
safe-first:1
safe-after-close:StopIteration
broken:caught:GeneratorExit
safe:cleanup