Desk Clarity

A field guide to everyday digital problems.

Developer tools / Practical guide

Python RuntimeError: generator raised StopIteration — Choose the Right Exit

Distinguish normal generator completion, invalid input and inner iterator exhaustion before fixing RuntimeError: generator raised StopIteration.

By ON Lab3 min readSources checked November 10, 2026 (KST)

When a Python generator ends with RuntimeError: generator raised StopIteration, find the StopIteration that escaped from its body. Do not suppress every RuntimeError at the consumer. Decide whether the producer should finish normally, report invalid input, or continue pulling values from another iterator.

A historical reader expected a validation helper's StopIteration to reach an outer handler. The helper ran inside a generator, so the generator boundary changed the failure they observed. That is a different problem from consuming the first item of a finite generator and then looping over the remaining items.

Read the chained exception before choosing a fix

Run this deliberately broken example in a disposable Python process:

def broken():
    yield "first"
    raise StopIteration("unexpected producer exit")

g = broken()
print(next(g))
next(g)

The first request yields one item. The second runs the remaining body and fails. Inspect the full traceback, including the earlier exception and its location. The line at the bottom names the converted error; the preceding traceback can identify the explicit raise or an exhausted inner next() call. Log exception types and code locations without copying private input into shared logs.

This conversion is the default for Python 3.7 and later. Adding a future import or downgrading an application is not an appropriate repair for an incorrect producer exit. Check the interpreter running the actual application when historical examples behave differently.

Use return for deliberate completion

When a negative value means “this stream is complete,” express that policy inside the generator:

def until_negative(source):
    for value in source:
        if value < 0:
            return
        yield value

assert list(until_negative([2, 0, -1, 3])) == [2, 0]

The return stops this generator. It does not mean “skip this item.” If the input contract instead says to skip negative values and keep scanning, use a separate filtering policy and test that later values remain present. Do not silently substitute one interpretation for the other.

Keep invalid input visible

If a negative input is an error, report an application exception such as ValueError. Put that decision in the validation path. Ending normally would make an incomplete result look successful to a consumer.

def require_nonnegative(source):
    for value in source:
        if value < 0:
            raise ValueError("negative input")
        yield value

A consumer may already have received values before the error occurs. Fixing the exception type does not roll back earlier writes or requests. Test using a local recorder before attaching side effects, and define how partial progress is handled.

Let iteration handle an exhausted inner source

For simple delegation, use yield from source. For per-item transformation, a for loop usually expresses the boundary directly. Both avoid an unguarded repeated next call in the generator body.

When manual pulling is necessary, a unique sentinel distinguishes exhaustion from a legitimate None, zero, False or empty string:

def manual(source):
    missing = object()
    while True:
        value = next(source, missing)
        if value is missing:
            return
        yield value

Here source must be an iterator. Call iter(collection) once before passing a collection. Do not recreate its iterator on each loop iteration. Using next(source, None) and yielding the result can manufacture a None item at exhaustion; checking truthiness can discard real values. The private object and identity test avoid those two mistakes.

The default only handles exhaustion signaled by next. It does not hide other exceptions from the source. An error in the source's own generator body can still become RuntimeError; inspect that producer rather than adding a blanket catch here.

Verify completion and failure separately

Our finite local fixture checks an escaped StopIteration, intentional return, invalid input, and empty or false-like input through delegation and manual pulling. It does not test async generators, production integrations, or replay safety. Re-run a minimal failing producer with the application's interpreter before changing its surrounding consumer.

For a related but separate task, see consuming a first item and iterating the remainder.

Sources & further reading