When a generator function returns normally, it implicitly raises a StopIteration exception. This is the standard signal to the consumer that the iteration is complete. However, the StopIteration exception is not just a signal; it can also carry a value, which becomes the return value of the generator function. This value is accessible as the value attribute of the StopIteration exception or, more commonly, as the return value of a yield from expression.

def countdown(n):
    while n > 0:
        yield n
        n -= 1
    return "Blastoff!"

gen = countdown(3)
try:
    print(next(gen))  # Output: 3
    print(next(gen))  # Output: 2
    print(next(gen))  # Output: 1
    print(next(gen))  # This will raise StopIteration
except StopIteration as e:
    print(e.value)    # Output: Blastoff!

The close() Method and Generator Cleanup

A generator’s lifecycle isn’t always allowed to run its natural course. A consumer might decide to stop iterating before the generator has exhausted itself. If the generator has open file handles, network connections, or other resources that need explicit cleanup, this premature termination could lead to resource leaks. This is where the close() method becomes critical.

Calling close() on a generator object signals that the consumer is finished with it. This method injects a GeneratorExit exception into the generator at the point where it was last suspended. The generator’s responsibility is to catch this exception and perform any necessary cleanup, such as closing files or releasing locks, often inside a finally: block. If the generator handles GeneratorExit and returns (or finishes execution), close() returns silently. If the generator yields a value instead of closing, close() raises a RuntimeError, indicating the generator failed to terminate promptly.

def read_lines(filename):
    print(f"Opening {filename}")
    f = open(filename, 'r')
    try:
        for line in f:
            yield line.strip()
    finally:
        print(f"Closing {filename}")
        f.close()  # This will always execute

# Normal exhaustion
file_reader = read_lines('data.txt')
for line in file_reader:
    print(line)
# Output: File is opened, lines are printed, then "Closing data.txt" is printed.

# Premature termination with close()
file_reader = read_lines('data.txt')
print(next(file_reader))  # Output: Opening data.txt \n FirstLine
file_reader.close()       # Output: Closing data.txt

Interaction of close() with try/finally

The finally block in a generator is the cornerstone of robust resource management. Its code is guaranteed to run when the generator is garbage-collected, whether it is exhausted, closed explicitly with close(), or even if an error occurs during iteration. This makes it the unequivocally correct place to put cleanup code. Relying on the generator being exhausted to trigger cleanup is a common pitfall; the finally block ensures cleanup happens regardless of how the generator’s life ends.

def monitored_generator():
    resource = "Acquired"
    print(f"Resource {resource}")
    try:
        yield "First value"
        yield "Second value"
    finally:
        print(f"Releasing {resource}") # This will run no matter what

gen = monitored_generator()
value = next(gen)
gen.close()  # Output: Resource Acquired \n Releasing Acquired

Pitfalls and Best Practices

A significant pitfall is ignoring the close() method and assuming resources will be freed upon garbage collection. While CPython’s garbage collector will eventually call the destructor (__del__) which triggers the finally block, this is not guaranteed in all Python implementations or in cases of circular references. Explicitly calling close() or using the generator in a context manager is always safer.

Another critical best practice is to write generators that are close()-safe. They should never suppress the GeneratorExit exception or attempt to yield more values after it has been raised. Doing so will cause a RuntimeError.

The most Pythonic way to ensure a generator is properly closed is to use it as a context manager with the contextlib.contextmanager decorator or, even better, with a with statement. This guarantees that close() is called when exiting the block, even if an exception occurs.

from contextlib import contextmanager

@contextmanager
def managed_resource(url):
    resource = connect_to_database(url)
    try:
        yield resource
    finally:
        resource.disconnect()

# Usage guarantees cleanup
with managed_resource('localhost:5432') as db:
    db.query('SELECT ...')