This whole track is about turning "loop and accumulate" into "generate and consume." Once you do, you write code that's easier to compose, cheaper on memory, and shorter. Here's the short list of patterns that compose well together.
Pattern 1 — yield instead of append-then-return
The clearest sign you should write a generator: a function that builds a list with append and returns it, where the caller iterates it once. Convert: drop the initial result = [], change result.append(x) to yield x, drop the return.
Pattern 2 — generator expression in a reducer
When you're going to feed a comprehension into sum, any, all, max, min, or set, drop the brackets. sum(x*x for x in nums) beats sum([x*x for x in nums]) on memory and short-circuiting.
Pattern 3 — pipeline over nested loop
If your big loop does several distinct steps, factor each into a generator. The resulting pipeline reads top-to-bottom and tests each layer independently.
Pattern 4 — itertools before custom code
Before writing your own iterator, check itertools. The combinatoric trio (product, permutations, combinations) replaces almost all hand-rolled nested loops. chain handles concatenation. islice handles slicing. groupby handles consecutive grouping.
Anti-pattern — turning lazy back into eager
Don't call list() in the middle of a pipeline unless you have a specific reason. Don't iterate the same iterator twice and wonder why the second pass is empty. Don't reach for tee when materializing as a list is simpler and clearer.
Pythonic Way: The skill of iteration is knowing when to materialize and when not to. Default to lazy. Materialize when you must — when you need len, indexing, slicing, or repeated iteration. The default of "build a list and return it" is a learned habit from languages without generators; Python lets you do better.
Code
Pattern 1 — yield instead of append·python
# Before — eager
def squares_v1(nums):
result = []
for n in nums:
result.append(n * n)
return result
# After — lazy
def squares_v2(nums):
for n in nums:
yield n * n
# Caller code is identical for `for x in squares(...)` consumption
# But v2 streams without ever building a list
for x in squares_v2(range(5)):
print(x)
# 0 1 4 9 16
Pattern 2 — generator expression in reducers·python
nums = [1, 2, 3, 4, 5]
# Better — no intermediate list, short-circuits when applicable
print(sum(x*x for x in nums)) # 55
print(any(x > 3 for x in nums)) # True
print(max(x*2 for x in nums)) # 10
print(set(x % 3 for x in nums)) # {0, 1, 2}
Pattern 3 — small composable stages·python
def even_only(nums):
for n in nums:
if n % 2 == 0:
yield n
def double(nums):
for n in nums:
yield n * 2
def summed(nums):
return sum(nums)
# Read top to bottom:
result = summed(double(even_only(range(10))))
print(result) # 40 (2+4+6+8) doubled = 40
Pattern 4 — itertools first·python
import itertools as it
# Don't write this
result = []
for x in [1, 2, 3]:
for y in ["a", "b"]:
result.append((x, y))
# Write this
result = list(it.product([1, 2, 3], ["a", "b"]))
print(result)
# [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b'), (3, 'a'), (3, 'b')]
Take a function process_log that opens a log file, builds a list of (timestamp, level, message) tuples by appending in a loop, returns the list, and the caller filters for errors. Refactor it into three pieces: (a) lines_of(path) — generator yielding stripped lines. (b) parsed(lines) — generator yielding the tuples. (c) errors_only(parsed) — generator yielding only tuples where level == "ERROR". Show that the original caller code (for ts, lvl, msg in errors_only(parsed(lines_of(path))):) needs no other change. Use a small in-memory string instead of a real file for testing.
Progress
Progress is local-only — sign in to sync across devices.