The close() method provides a mechanism to gracefully terminate a generator coroutine from the outside. When invoked, it raises a GeneratorExit exception inside the generator at the point where its execution is suspended—typically at a yield expression. This exception is not meant to be caught and handled like a typical error; rather, it signals to the generator that it should perform any necessary cleanup (like closing open files or network connections) and then terminate. If the generator handles the exception and yields another value instead of stopping, the close() method will raise a RuntimeError, as this violates the protocol for graceful termination.

How close() Propagates GeneratorExit

When close() is called on a generator object, the interpreter injects a GeneratorExit exception into the generator’s frame. This exception is raised at the exact location of the suspended yield. The generator’s response to this exception dictates what happens next. The intended flow is for the generator to catch this exception, execute any finally blocks or context manager __exit__ methods in its current scope, and then exit. The close() method returns None upon successful completion.

def simple_coroutine():
    try:
        while True:
            value = yield
            print(f"Received: {value}")
    except GeneratorExit:
        print("Generator is closing. Performing cleanup.")
    finally:
        print("This finally block always runs.")

gen = simple_coroutine()
next(gen)  # Prime the generator
gen.send('hello')  # Received: hello
gen.close()  # Generator is closing. Performing cleanup. \n This finally block always runs.

The Role of finally and with Statements

The most robust way to handle resource cleanup in a generator is not by catching GeneratorExit explicitly, but by using finally blocks or context managers (with statements). These constructs are guaranteed to execute when the generator is garbage-collected, whether via an explicit close(), a break from a loop consuming the generator, or simply the reference going out of scope. This makes code more resilient and less prone to resource leaks.

def coroutine_with_resource():
    # Simulate opening a resource
    print("Acquiring resource")
    try:
        while True:
            data = yield
            print(f"Processing: {data}")
    finally:
        # This will execute on close() or garbage collection
        print("Releasing resource")

gen = coroutine_with_resource()
next(gen)
gen.send('data A')  # Processing: data A
gen.close()  # Releasing resource

Pitfall: Yielding After GeneratorExit

A critical rule is that a generator must not yield a value after GeneratorExit has been raised inside it. If a generator catches GeneratorExit and then attempts to yield again, the close() method will escalate this protocol violation into a RuntimeError. This prevents the generator from continuing its operation after it has been formally signaled to terminate.

def misbehaving_coroutine():
    try:
        value = yield
        print(f"Got: {value}")
    except GeneratorExit:
        print("Exiting, but now I'll yield again...")
        yield "This is illegal!"  # This line causes the RuntimeError

gen = misbehaving_coroutine()
next(gen)
gen.send('test')
try:
    gen.close()  # Exiting, but now I'll yield again...
except RuntimeError as e:
    print(f"Caught RuntimeError: {e}") # Caught RuntimeError: generator ignored GeneratorExit

Interaction with throw() and Exhausted Generators

It is important to note that close() has no effect on a generator that has already been exhausted (i.e., has raised a StopIteration naturally or via an unhandled exception). Calling close() on an exhausted generator is a no-op and simply returns None. Furthermore, close() is distinct from throw(). While close() injects the specific GeneratorExit exception, throw() can inject any exception type. However, if GeneratorExit is injected via throw(), it is treated exactly as if close() had been called, triggering the same cleanup and termination process.

def exhausted_coroutine():
    yield 1
    yield 2
    # Generator exits naturally

gen = exhausted_coroutine()
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen, 'Default')) # Default (generator is exhausted)
# Closing an exhausted generator is safe and does nothing.
gen.close() # No error, returns None

Best Practices for Robust Coroutines

  1. Prefer finally over except GeneratorExit: For cleanup code, rely on finally blocks. This ensures cleanup runs for all termination paths, including close(), throw(), and natural exhaustion.
  2. Use Context Managers: For managing resources (files, sockets, locks), use with statements inside the generator. This is the most Pythonic and reliable method.
  3. Don’t Swallow GeneratorExit: If you must catch GeneratorExit for logging or specific actions, re-raise it or ensure the generator exits immediately afterward. Never yield a value after receiving it.
  4. Call close() Explicitly: When you are done with a generator coroutine that manages resources, explicitly call close(). While garbage collection will eventually do this, explicit management is safer and more predictable.