Back to Blog
Python

Python Generator close: Deterministic Cleanup

python generator close: Learn how Python's generator close() method triggers GeneratorExit, runs finally blocks, and enables deterministic resource cleanup in your code.

PythonGeneratorsGeneratorExitResource Cleanupcontextlib
Illustration of a Python generator being closed with a shield representing resource cleanup.

Calling close() on a Python generator is not just a formality. It triggers a specific control flow that can run cleanup code, release resources, and propagate exceptions. Understanding exactly what close() does matters when a generator holds file handles, database connections, or other state that must be released deterministically. The python generator close mechanism is part of the generator protocol, and using it correctly prevents resource leaks and subtle runtime errors.

The close() Method and GeneratorExit

Every generator object has a close() method. When you call it, Python raises a GeneratorExit exception at the point where the generator is currently suspended. If the generator is running, the exception is thrown into the generator frame. The generator can catch this exception, but it must not yield another value; doing so raises a RuntimeError.

Consider this simple generator:

def countdown(): try: yield 3 yield 2 yield 1 finally: print("cleanup") gen = countdown() print(next(gen)) # 3 gen.close() # prints "cleanup"

Here, close() causes the generator to resume, hit the finally block, and then terminate. The GeneratorExit is raised at the yield expression, so any code in a finally block runs as part of the shutdown sequence. This is the primary way to ensure deterministic cleanup when you stop consuming a generator early.

Why Closing a Generator Matters

Generators often wrap external resources. A generator that reads lines from a file, for example, might hold the file open until it is fully consumed or explicitly closed. If you stop iterating early, the file remains open unless you call close() or rely on garbage collection. Garbage collection is not deterministic, especially in CPython where reference counting may close it quickly, but in other Python implementations or when cycles are involved, cleanup can be delayed indefinitely.

def read_lines(path): f = open(path) try: for line in f: yield line finally: f.close()

If you only read a few lines and then abandon the generator, the file stays open until the generator is garbage collected. Calling close() forces the finally block to execute immediately, releasing the file descriptor. This is especially important in long-running processes where leaked descriptors accumulate.

How close() Works with try/finally Blocks

The finally block is the natural place to put cleanup logic. When close() is called, the generator resumes execution, raises GeneratorExit at the current yield, and then executes any finally clauses. After the finally block completes, the generator is closed and cannot be resumed.

If a generator does not have a try/finally around the yield, close() simply stops the generator without running any custom cleanup. This is fine for generators that do not hold external state, but it is easy to forget that no cleanup will happen.

def simple_gen(): yield 1 yield 2 gen = simple_gen() next(gen) gen.close() # no cleanup code runs

There is no exception here; the generator just stops. But if the generator had allocated a resource, that resource would leak. Always wrap resource-holding generators in try/finally.

Closing a Generator That Is Already Exhausted

Calling close() on a generator that has already finished normally is a no-op. The generator is already closed, so close() does nothing. This is safe and does not raise an error.

def finished_gen(): yield 1 gen = finished_gen() list(gen) # exhausts the generator gen.close() # no effect

This behavior is convenient because you can call close() unconditionally without worrying about the generator's state. It also means that contextlib.closing can be used safely around generators that may be fully consumed.

Using contextlib.closing for Automatic Cleanup

The contextlib.closing context manager calls close() on its target when the with block exits. This is useful when you want to guarantee that a generator is closed even if an exception occurs during iteration.

from contextlib import closing def gen_with_resource(): try: yield 1 yield 2 finally: print("resource released") with closing(gen_with_resource()) as gen: print(next(gen)) # If an exception occurs here, close() is still called.

This pattern is particularly helpful when you need to pass a generator to another function that may not consume it fully. By wrapping it in closing, you ensure that the generator's cleanup logic runs as soon as the with block exits, regardless of how many items were consumed.

Interaction with yield from and Nested Generators

When a generator uses yield from to delegate to a subgenerator, calling close() on the outer generator propagates the close to the inner generator as well. This is part of the generator protocol: close() on the outer generator raises GeneratorExit in the outer generator, which then propagates into the yield from expression, causing the subgenerator to be closed too.

def inner(): try: yield 1 yield 2 finally: print("inner cleaned") def outer(): yield from inner() gen = outer() next(gen) gen.close() # prints "inner cleaned"

This makes it easier to compose resource-managing generators without manually forwarding close() calls. However, if a subgenerator catches GeneratorExit and tries to yield again, the same RuntimeError rule applies, and the exception propagates up.

Performance and Memory Considerations

From a performance perspective, close() is not about speed; it is about deterministic resource release. Relying on garbage collection to clean up generators can lead to unpredictable memory usage, especially in server applications that create many short-lived generators. Calling close() when you are done with a generator ensures that any finally blocks execute promptly, which can reduce peak memory usage and prevent file descriptor exhaustion.

There is a small runtime cost to calling close(): it raises an exception inside the generator and unwinds the stack. This is negligible compared to the cost of leaking a resource. In tight loops where generators are created and abandoned frequently, the overhead of close() is usually acceptable, but if you are in a performance-critical path, you might prefer to structure your code so that generators are fully consumed or use context managers instead.

Common Pitfalls When Closing Generators

One common mistake is catching GeneratorExit and then attempting to yield another value. The generator protocol forbids this. If you catch GeneratorExit and yield, Python raises a RuntimeError that will propagate out of close(), potentially breaking the caller.

def bad_gen(): try: yield 1 except GeneratorExit: yield 2 # RuntimeError! gen = bad_gen() next(gen) gen.close() # raises RuntimeError

Another pitfall is forgetting that close() does not run cleanup code unless the generator has a try/finally block. If you are using a generator that internally acquires a resource, make sure the resource is released in a finally block, not just at the end of the function. The generator might be closed before it reaches that end.

Finally, be careful when using close() in code that also closes the generator through other means, such as contextlib.closing or a custom wrapper. Duplicate close() calls are safe, but they may run the same cleanup logic twice if the generator is not properly guarded. Usually, finally blocks are idempotent, but if they are not, you should design them to handle multiple invocations gracefully.

Understanding python generator close is about more than just calling a method. It is about controlling the lifecycle of a generator and ensuring that the resources it holds are released at the right time. By using close() deliberately and pairing it with try/finally, you can write generators that behave predictably in production.

python generator close: Deterministic Cleanup | RYUSLOG DEV