GeneratorExit
close() throws it into the generator at the paused yield — let it propagate (or return), never yield again.
GeneratorExit(*args)
Raised by
gen.close(), garbage collection of a suspended generator, coroutine.close()
Message
empty — you normally never see it
Quick fix
cleanup in finally; after GeneratorExit, return or re-raise
Watch out
yielding after it → RuntimeError: generator ignored GeneratorExit
Demo
Live evaluation
Advance the generator n times, then close() it. GeneratorExit appears at the paused yield — if there is one.
Try:
Inputs
nintnext() calls before close()
Code
log = [] def reader(): try: while True: yield 'line' except GeneratorExit: log.append('GeneratorExit received') raise g = reader() for _ in range(2): next(g) g.close() log
Result
['GeneratorExit received']
With n = 0 the log stays empty: a generator that never started has no paused yield to throw into, so close() just marks it finished. Once started, close() raises GeneratorExit at the yield; the handler re-raises it and close() returns normally. In Handle, returning instead of re-raising is also allowed — and in 3.13+ close() gives you that value.
Constructor
| Name | Type | Required | Description |
|---|---|---|---|
| *args | object | no | Unused in practice; close() throws it without arguments. |
Attributes
| Attribute | Type | Meaning |
|---|---|---|
| args | tuple | Empty when thrown by close(). |
| __traceback__ | traceback | None | Points at the yield where the generator was paused. |
Common patterns
Cleanup belongs in finally
finally runs on close(), on exhaustion and on errors — except GeneratorExit only on close().
def read_rows(path): f = open(path) try: for line in f: yield line.rstrip('\n') finally: f.close()
Close deterministically
Do not rely on garbage collection to close a half-consumed generator — contextlib.closing calls close() on exit.
from contextlib import closing with closing(read_rows('data.csv')) as rows: header = next(rows)
Return a result when closed (3.13+)
A generator used as a sink can return its summary; close() returns it.
def averager(): total = count = 0 try: while True: total += yield count += 1 except GeneratorExit: return total / count if count else None
Examples
1. Not an Exception subclass
issubclass(GeneratorExit, Exception)
Returns
False2. close() runs finally
log = []
def gen():
try:
yield 1
yield 2
finally:
log.append('cleanup')
g = gen()
next(g)
g.close()
log
Returns
['cleanup']3. except Exception does not intercept it
def gen():
while True:
try:
yield
except Exception:
pass
g = gen()
next(g)
g.close()
'closed cleanly'
Returns
'closed cleanly'4. Closing an unstarted generator runs nothing
log = []
def gen():
try:
yield 1
finally:
log.append('cleanup')
g = gen()
g.close()
log
Returns
[]5. close() returns the return value (3.13+)
def gen():
try:
yield
except GeneratorExit:
return 'cleanup done'
g = gen()
next(g)
g.close()
Returns
'cleanup done'6. break closes it (CPython, when freed)
log = []
def gen():
try:
yield from range(10)
finally:
log.append('closed')
for x in gen():
if x == 2:
break
log
Returns
['closed']7. close() on a finished generator is a no-op
def gen():
yield 1
g = gen()
list(g)
g.close()
'ok'
Returns
'ok'Pitfalls
1. Swallowing GeneratorExit and yielding again
If the generator catches GeneratorExit and reaches another yield, close() raises RuntimeError. Return (or re-raise) instead.
pass and loop
def ticker(): while True: try: yield 'tick' except GeneratorExit: pass g = ticker() next(g) g.close()
RuntimeError: generator ignored GeneratorExit
return
def ticker(): while True: try: yield 'tick' except GeneratorExit: return g = ticker() next(g) g.close() 'closed'
'closed'
2. A bare except inside a generator
except: catches GeneratorExit too, so the loop carries on to the next yield. except Exception lets it through.
bare except
def safe_items(items): for x in items: try: yield int(x) except: continue g = safe_items(['1', '2', '3']) next(g) g.close()
RuntimeError: generator ignored GeneratorExit
except Exception
def safe_items(items): for x in items: try: yield int(x) except Exception: continue g = safe_items(['1', '2', '3']) next(g) g.close() 'closed'
'closed'
3. Cleanup only in except GeneratorExit
That handler runs only on close(). When the generator is consumed to the end, the cleanup never happens.
except GeneratorExit
log = [] def gen(): try: yield 1 except GeneratorExit: log.append('cleanup') raise list(gen()) log
[]
finally
log = [] def gen(): try: yield 1 finally: log.append('cleanup') list(gen()) log
['cleanup']
When to use
Use it
- Knowing why finally blocks in generators run when a loop breaks early
- except GeneratorExit: return result — a sink generator that reports on close (3.13+)
- Debugging RuntimeError: generator ignored GeneratorExit
Reach for something else
- General cleanup → try/finally or a with block inside the generator
- Raising it yourself → call gen.close() instead
- Catching it in a bare except → catch Exception
Notes
CPython impl
Objects/genobject.c — gen_close() throws GeneratorExit; if the generator yields again it raises RuntimeError("generator ignored GeneratorExit")
Why BaseException
The docs: it is technically not an error, so except Exception in generator code must not swallow it
Garbage collection
A suspended generator that is garbage-collected is closed; in CPython that happens as soon as the last reference goes away
Coroutines
coroutine.close() and async generator aclose() use the same mechanism
FAQ
The generator caught GeneratorExit (often via a bare except: or except BaseException:) and then yielded another value. A closing generator must finish: re-raise the GeneratorExit, return, or let it propagate. Replace the bare except with except Exception.
History
3.13
If a generator returns a value upon being closed, the value is returned by close().