Notes / NumPy & Python
Python Generators — `yield`, `yield from`, and typing
Table of Contents
Generators produce values lazily (on demand), so they represent huge or infinite sequences with O(1) memory instead of materializing a whole list.
yield — the basics
yield turns a function into a generator function. Calling it doesn’t run the body — it returns a
generator object; the body runs lazily, one piece at a time.
def counter(): print("start") yield 1 yield 2 yield 3
g = counter() # nothing printed — body hasn't run yetnext(g) # prints "start", returns 1 → SUSPENDS herenext(g) # resumes after `yield 1`, returns 2next(g) # returns 3next(g) # body ends → raises StopIterationMental model: yield produces a value and suspends the function, freezing all local state
(variables, loop position). The next next() resumes exactly where it left off. That’s what makes
generators lazy:
def naturals(): # infinite — impossible as a list n = 0 while True: yield n n += 1A for loop is just repeated next() until StopIteration:
for x in counter(): # next() under the hood, stops cleanly at StopIteration ...Two-way (advanced): yield is also an expression — value = yield x sends x out and receives
whatever the caller passes via g.send(...). Basis of coroutines; rarely needed for plain iteration.
yield from — delegating to a sub-iterable
yield from iterable yields every item from that iterable. Naive expansion:
yield from sub# ≡for item in sub: yield itemFlattening / recursion (treat str as an atomic leaf — see the string gotcha below):
def flatten(x): for item in x: if isinstance(item, (list, tuple)): yield from flatten(item) # delegate to the recursive call else: yield itemChaining sequences:
def chain_all(*iterables): for it in iterables: yield from it # concatenate several iterables into one streamyield from is more than a loop
It sets up a transparent two-way channel between the outer caller and the delegated inner
generator — the part a plain for-loop version can’t do:
- Values flow out from inner to caller.
send()/throw()from the caller are forwarded into the inner generator.- The inner generator’s
returnvalue becomes the value of theyield fromexpression:
def inner(): yield 1 yield 2 return "done" # captured, NOT yielded
def outer(): result = yield from inner() # result == "done" after inner finishes print(result) # "done" yield 3Point 3 is the one people miss: inside a generator, return value doesn’t yield value — it stashes
it in StopIteration, and yield from is what lets you capture it.
The string gotcha (flattening nested strings)
A str is itself iterable, so a naive “recurse if iterable” flattener explodes "abc" into
'a','b','c'. Guard by recursing only into list/tuple (as above), not
isinstance(item, Iterable) — otherwise strings get shredded character-by-character.
Typing a generator
The return type annotates what it yields, not the generator object (see type-annotations):
from collections.abc import Iterator, Generator
def squares(n: int) -> Iterator[int]: # common case: only yields for i in range(n): yield i * i
def gen() -> Generator[YieldT, SendT, ReturnT]: # full form — only if you use send/return ...Iterator[T]for a plain generator — the right default.Generator[Y, S, R]only when you use.send()(S) or a meaningfulreturnvalue (R). Unused slots areNone:Generator[int, None, None]≡Iterator[int].- Async generators (
async def+yield) →AsyncIterator[T]/AsyncGenerator[Y, S]. - Use
collections.abc, not the deprecatedtyping.Iterator/Generator.
The distinction to state crisply
yield x— produce one value, suspend, resume later.yield from iterable— produce all values from a sub-iterable, and (for generators) transparently forwardsend/throwand capture the sub-generator’sreturnvalue.
Use yield to emit individual items; use yield from when delegating to another iterable/generator
(recursion, chaining, composition) — more readable and semantically richer than the manual for loop.
Related: type-annotations, python-basics
