Python 제너레이터의 return 값은 어디로 갈까: StopIteration.value와 PEP 479
제너레이터에서 return한 값은 for 문에서는 보이지 않고 StopIteration.value에 담깁니다. yield from으로 받는 법, 제너레이터 안의 StopIteration이 RuntimeError가 되는 이유, close()의 반환값을 정리했습니다.
제너레이터 함수 안에도 return 값을 쓸 수 있습니다. 문법 오류도 아니고 경고도 나오지 않습니다. 그런데 이 제너레이터를 for 문이나 list()로 돌리면 그 값은 어디에도 나타나지 않습니다. 값이 사라진 것은 아니고, 반복이 끝났다는 신호인 StopIteration 예외 안에 들어 있습니다.
return은 StopIteration을 만든다
언어 레퍼런스의 return 문 설명은 이렇습니다. 제너레이터 함수에서 return은 제너레이터가 끝났음을 뜻하고 StopIteration을 발생시키며, 반환한 값이 있으면 그 값으로 StopIteration을 만들어 StopIteration.value 속성이 됩니다. value는 아무것도 넘기지 않으면 None입니다.
def read_items():
yield "a"
yield "b"
return 2 # 읽은 개수
gen = read_items()
print(next(gen)) # a
print(next(gen)) # b
try:
next(gen)
except StopIteration as e:
print(e.value) # 2
next()를 직접 부르고 StopIteration을 잡아야 값을 볼 수 있습니다. for 문 명세는 반복자가 내놓는 항목을 하나씩 대입하고, 반복자가 소진되면 else 절을 실행한 뒤 끝난다고만 설명합니다. 마지막 StopIteration에 담긴 값을 받는 문법은 없습니다. 그래서 아래 코드는 "a", "b"만 다루고 2는 버려집니다.
for item in read_items():
print(item)
반환값은 Python 3.3에서 생겼습니다. 내장 예외 문서에 따르면 3.3에서 value 속성과, 제너레이터 함수가 이 속성으로 값을 돌려주는 기능이 함께 추가되었습니다.
값을 받는 쪽은 yield from
이 값을 자연스럽게 받는 방법은 다른 제너레이터 안에서 yield from으로 위임하는 것입니다. 레퍼런스는 하위 반복자가 끝나면 발생한 StopIteration의 value가 yield from 식의 값이 되고, 하위 반복자가 제너레이터라면 그 제너레이터가 return한 값이 자동으로 들어간다고 설명합니다.
def outer():
count = yield from read_items()
yield f"{count}개 읽음"
print(list(outer())) # ['a', 'b', '2개 읽음']
yield from이 하는 일은 값을 받는 것만이 아닙니다. 하위 반복자가 만든 값은 바깥 제너레이터를 호출한 쪽으로 그대로 전달되고, send()로 보낸 값과 throw()로 던진 예외도 하위 반복자가 해당 메서드를 갖고 있으면 그쪽으로 넘어갑니다. 반복문을 돌며 yield를 다시 쓰는 코드로 바꾸면 이 전달과 반환값이 함께 빠집니다.
제너레이터 안의 StopIteration은 RuntimeError가 된다
반환값과 별개로, 제너레이터 안에서 StopIteration을 직접 다루면 다른 일이 생깁니다. 내장 예외 문서는 제너레이터 코드가 직접이든 간접이든 StopIteration을 발생시키면 그것이 RuntimeError로 바뀌고, 원래 StopIteration은 새 예외의 원인(__cause__)으로 남는다고 적고 있습니다. PEP 479의 내용입니다.
간접적인 경우가 흔합니다. 다른 반복자에서 next()를 기본값 없이 부르면, 그 반복자가 비었을 때 StopIteration이 나옵니다.
def pairs(it):
it = iter(it)
while True:
first = next(it)
second = next(it)
yield first, second
list(pairs([1, 2, 3, 4]))
PEP 479 이전에는 next(it)에서 나온 StopIteration이 제너레이터 바깥에서 정상적인 반복 종료처럼 보였습니다. 그래서 이 코드는 입력이 끝나면 조용히 멈췄고, 버그로 생긴 StopIteration도 똑같이 조용히 멈췄습니다. 지금 규칙에서는 입력이 다 떨어지는 순간 RuntimeError가 납니다. 짝수 개 입력에서도 마찬가지입니다. 다섯 번째 next(it)에서 StopIteration이 나기 때문입니다.
의도한 종료라면 return으로 끝냅니다.
_END = object()
def pairs(it):
it = iter(it)
while True:
first = next(it, _END)
if first is _END:
return
second = next(it, _END)
if second is _END:
return # 짝이 없는 마지막 원소는 버림
yield first, second
next()에 기본값을 주면 반복자가 소진되었을 때 StopIteration 대신 그 값을 돌려줍니다. 입력에 None이 들어올 수 있다면 None보다 전용 표식 객체를 쓰는 편이 안전합니다. StopIteration을 try로 잡고 return해도 됩니다. 핵심은 제너레이터 밖으로 StopIteration이 새어 나가지 않게 하는 것입니다. 같은 이유로 제너레이터 안에서 raise StopIteration(값)을 쓰던 코드는 return 값으로 바꿔야 합니다.
이 변환은 제너레이터(와 코루틴)에만 적용됩니다. 클래스로 만든 반복자의 __next__()는 지금도 StopIteration을 발생시켜 반복이 끝났음을 알려야 합니다.
| 버전 | 동작 |
|---|---|
| 3.5 | from __future__ import generator_stop을 쓴 모듈에서만 변환 |
| 3.7 | 모든 코드에서 기본으로 변환 (__future__ 표에서 mandatory) |
3.7 변경 사항 문서도 PEP 479가 모든 코드에 켜져서, 코루틴과 제너레이터 안에서 직접이든 간접이든 발생한 StopIteration이 RuntimeError로 바뀐다고 적고 있습니다. 3.6 이하에서 작성된 코드를 옮길 때 이 차이로 동작이 달라질 수 있습니다.
close()도 반환값을 돌려준다 (3.13부터)
close()는 제너레이터가 멈춰 있는 지점에 GeneratorExit을 발생시킵니다. 제너레이터가 이 예외를 잡고 값을 return하면, Python 3.13부터는 그 값이 close()의 반환값이 됩니다. 이전 버전에서는 None이었습니다.
def summer():
total = 0
try:
while True:
total += yield
except GeneratorExit:
return total
acc = summer()
next(acc) # 첫 yield까지 진행
acc.send(3)
acc.send(4)
print(acc.close()) # 3.13 이상: 7, 이전: None
next(acc)를 먼저 부르는 이유는 send()로 제너레이터를 시작할 때는 받을 yield 식이 아직 없어서 인자가 None이어야 하기 때문입니다. close() 문서에는 몇 가지 경우가 더 있습니다. 이미 끝난 제너레이터이거나 GeneratorExit을 잡지 않고 그대로 내보내면 None을 돌려주고, GeneratorExit을 받은 뒤 다시 yield하면 RuntimeError가 납니다. 그 밖의 예외는 호출한 쪽으로 전파됩니다.
비동기 제너레이터에는 반환값이 없다
async def 안에 yield가 있는 비동기 제너레이터에서는 값을 가진 return이 문법 오류입니다. 빈 return만 쓸 수 있고, 이때 발생하는 것은 StopAsyncIteration입니다. 동기 제너레이터에서 반환값을 쓰던 설계를 비동기로 옮길 때는 결과를 마지막 yield로 내보내거나 별도 객체에 담는 식으로 바꿔야 합니다.
정리하면
- 제너레이터의
return 값은StopIteration.value에 담긴다.for,list()로는 보이지 않는다. - 값을 받으려면
yield from으로 위임하거나next()에서StopIteration을 직접 잡는다. - 제너레이터 안에서 새어 나온
StopIteration은 3.7부터RuntimeError가 된다. 끝내고 싶으면return을 쓰고, 다른 반복자의next()에는 기본값을 주거나 예외를 잡는다. - 3.13부터
close()는GeneratorExit처리 중return한 값을 돌려준다.
예외가 조용히 사라지는 다른 사례로는 Java의 try-with-resources와 suppressed 예외를, Python 함수 정의 시점의 함정은 기본 인자에 []를 쓰면 안 되는 이유를 참고하세요.
이 글의 동작 설명은 2026년 10월 18일 Python 3.15.0 문서를 기준으로 확인했습니다. 예제 코드의 출력은 문서에 적힌 규칙에서 따라 나오는 결과로 적었고, 이 글을 위해 직접 실행한 결과는 아닙니다.
참고한 문서
- Python 3.15.0 언어 레퍼런스: return 문, yield 식, for 문
- 내장 예외: StopIteration, GeneratorExit
- 내장 타입: 제너레이터 반복자 메서드
- 내장 함수: next()
__future__: generator_stop- 변경 사항: Python 3.7