contextlib: suppress(GeneratorExit) Breaks Generator close() (tested)

Calling generator.close() works by throwing GeneratorExit into the generator at its current yield point -- the generator is expected to let that exception propagate out, not catch it. contextlib.suppress(GeneratorExit) will catch it like any other exception if it happens to be in scope. We measured what that does to close().

1. Swallowing GeneratorExit with no yield left afterward looks fine

import contextlib

def gen():
    with contextlib.suppress(GeneratorExit):
        yield "first"
        print("this never prints -- close() throws here")
    print("after suppress block")

g = gen()
next(g)
g.close()
print("closed cleanly")
after suppress block
closed cleanly

Measured: close() throws GeneratorExit at the yield, suppress(GeneratorExit) catches it, and the generator function then runs to completion normally (printing "after suppress block" and falling off the end). Because the generator terminates by returning rather than yielding again, close() is satisfied -- this case gives no sign anything unusual happened.

2. If anything reachable after the suppressed exit can yield again, close() breaks

import contextlib

def gen():
    with contextlib.suppress(GeneratorExit):
        yield "first"
    yield "second"  # reached because GeneratorExit was swallowed

g = gen()
print(next(g))
try:
    g.close()
    print("closed cleanly")
except RuntimeError as e:
    print("close error:", e)
first
close error: generator ignored GeneratorExit

Measured: once GeneratorExit is swallowed, the generator function keeps running as if nothing happened, and reaches yield "second". Python's generator protocol requires that a generator stop (return or raise) in response to GeneratorExit, not yield again -- close() enforces that by raising RuntimeError: generator ignored GeneratorExit. The error is raised from the close() call site, far from the suppress(GeneratorExit) that actually caused it.

3. The same thing happens with a bare try/except, so this isn't suppress-specific -- it's a trap for anything that catches GeneratorExit

import contextlib

def gen():
    try:
        yield "first"
    except GeneratorExit:
        pass
    yield "second"

g = gen()
next(g)
try:
    g.close()
except RuntimeError as e:
    print("close error:", e)
close error: generator ignored GeneratorExit

Measured: identical outcome with plain try/except GeneratorExit: pass. contextlib.suppress() doesn't add any special risk here -- it's simply a one-liner that makes it easy to catch a broader set of exceptions than intended, and GeneratorExit is one that generator code specifically must not catch-and-continue from.

Scope suppress() to real exceptions, never GeneratorExit

import contextlib

def gen():
    with contextlib.suppress(ValueError, KeyError):  # not GeneratorExit
        yield "first"

If a generator needs cleanup code that must run even when the caller calls close(), use try/finally around the yield instead -- finally runs during the GeneratorExit unwind without suppressing it, so the generator still stops as the protocol requires.

Frequently asked questions

Does contextlib.suppress() catch GeneratorExit if you pass it explicitly?

Yes -- we measured suppress(GeneratorExit) catching the exception close() throws into a generator just like any other listed exception type, with no special-casing.

What happens if a generator that caught GeneratorExit reaches another yield?

generator.close() raises RuntimeError: generator ignored GeneratorExit -- we measured this happening whether the catch was contextlib.suppress(GeneratorExit) or a plain try/except GeneratorExit: pass.

Does this only happen with contextlib.suppress()?

No -- we measured the identical RuntimeError from a bare try/except GeneratorExit block, confirming this is a generator-protocol rule (don't yield again after catching GeneratorExit), not something specific to suppress().

How do you run cleanup code in a generator without breaking close()?

Use try/finally around the yield instead of catching GeneratorExit -- finally runs during the close()-triggered unwind without suppressing the exception, so the generator still terminates the way close() requires.

Written with AI assistance. Measured on 2026-10-08 with Python 3.13.5: wrapped a yield in a generator with contextlib.suppress(GeneratorExit), then called close() on the generator both with and without a second yield reachable afterward. PlainHeaders is our extension; Python's contextlib module is built in.