Python · Syntax · Advanced
Generators and itertools
Functions that yield values one at a time instead of building a full list. Covers `yield`, `yield from`, generator expressions, and itertools for efficient pipelines.
Quick topic start and explanations before exercises (exercises below):
yield from, send(), throw(), close(), and infinite generators
#itertools reference, pipeline patterns, and generator vs list guide
#Exercises:
Fibonacci generator
#Write a generator function `fibonacci()` that yields Fibonacci numbers indefinitely (0, 1, 1, 2, 3, 5, 8, ...). Use `itertools.islice` to get the first `n` values from it.
import itertools
def fibonacci():
pass
first10 = list(itertools.islice(fibonacci(), 10))
print(first10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Solution
import itertools
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
first10 = list(itertools.islice(fibonacci(), 10))
print(first10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Custom range generator
#Write a generator function `frange(start, stop, step)` that yields float values from `start` up to (but not including) `stop` with the given `step`. It should work like `range()` but support float steps.
def frange(start, stop, step):
pass
print(list(frange(0, 1, 0.25))) # [0, 0.25, 0.5, 0.75]
print(list(frange(1, 2, 0.3))) # [1, 1.3, 1.6, 1.9]
Solution
def frange(start, stop, step):
current = start
while current < stop:
yield current
current += step
print(list(frange(0, 1, 0.25))) # [0, 0.25, 0.5, 0.75]
print(list(frange(1, 2, 0.3))) # [1, 1.3, 1.6000000000000001, 1.9]
Flatten nested lists with yield from
#Write a generator `flatten(nested)` that recursively flattens a nested list of any depth and yields individual items. Use `yield from` for recursion. Items that are not lists should be yielded as-is.
def flatten(nested):
pass
data = [1, [2, 3], [4, [5, 6]], [[7], 8]]
print(list(flatten(data))) # [1, 2, 3, 4, 5, 6, 7, 8]
Solution
def flatten(nested):
for item in nested:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
data = [1, [2, 3], [4, [5, 6]], [[7], 8]]
print(list(flatten(data))) # [1, 2, 3, 4, 5, 6, 7, 8]
Running average with send()
#Write a coroutine generator `running_average()` that computes a running average. Each call to `.send(value)` should add `value` to the total and `yield` the current average. Prime the generator with `next()` before sending values.
def running_average():
pass
avg = running_average()
next(avg) # prime the coroutine
print(avg.send(10)) # 10.0
print(avg.send(20)) # 15.0
print(avg.send(30)) # 20.0
Solution
def running_average():
total = 0
count = 0
value = yield # prime point — first next() pauses here
while True:
total += value
count += 1
value = yield total / count
avg = running_average()
next(avg) # advance to first yield
print(avg.send(10)) # 10.0
print(avg.send(20)) # 15.0
print(avg.send(30)) # 20.0
Sliding window generator
#Write a generator `sliding_window(iterable, n)` that yields tuples of `n` consecutive elements from the iterable, sliding one step at a time. For example, `sliding_window([1,2,3,4,5], 3)` yields `(1,2,3)`, `(2,3,4)`, `(3,4,5)`.
from collections import deque
def sliding_window(iterable, n):
pass
print(list(sliding_window([1, 2, 3, 4, 5], 3)))
# [(1, 2, 3), (2, 3, 4), (3, 4, 5)]
Solution
from collections import deque
def sliding_window(iterable, n):
window = deque(maxlen=n)
for item in iterable:
window.append(item)
if len(window) == n:
yield tuple(window)
print(list(sliding_window([1, 2, 3, 4, 5], 3)))
# [(1, 2, 3), (2, 3, 4), (3, 4, 5)]
take and drop generators
#Write two generator functions: - `take(n, iterable)` — yields the first `n` items from the iterable. - `drop(n, iterable)` — skips the first `n` items and yields the rest. Both should work lazily (do not convert to list internally).
def take(n, iterable):
pass
def drop(n, iterable):
pass
nums = range(10)
print(list(take(3, nums))) # [0, 1, 2]
print(list(drop(7, nums))) # [7, 8, 9]
Solution
def take(n, iterable):
for i, item in enumerate(iterable):
if i >= n:
break
yield item
def drop(n, iterable):
for i, item in enumerate(iterable):
if i >= n:
yield item
nums = range(10)
print(list(take(3, nums))) # [0, 1, 2]
print(list(drop(7, nums))) # [7, 8, 9]
# Or with itertools:
import itertools
list(itertools.islice(nums, 3)) # take(3)
list(itertools.islice(nums, 7, None)) # drop(7)
Chunk generator
#Write a generator `chunks(iterable, n)` that splits the iterable into consecutive chunks of size `n`. The last chunk may be smaller if the iterable length is not divisible by `n`. Yield each chunk as a list.
def chunks(iterable, n):
pass
print(list(chunks(range(10), 3)))
# [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
Solution
def chunks(iterable, n):
chunk = []
for item in iterable:
chunk.append(item)
if len(chunk) == n:
yield chunk
chunk = []
if chunk: # yield the last partial chunk
yield chunk
print(list(chunks(range(10), 3)))
# [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
# Python 3.12+: itertools.batched(iterable, n)
Prime number sieve generator
#Write a generator `primes()` that yields prime numbers indefinitely using a simple trial-division approach: for each candidate number, check divisibility only by previously found primes. Use `itertools.islice` to get the first 10 primes.
import itertools
def primes():
pass
print(list(itertools.islice(primes(), 10)))
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Solution
import itertools
def primes():
found = []
candidate = 2
while True:
if all(candidate % p != 0 for p in found):
found.append(candidate)
yield candidate
candidate += 1
print(list(itertools.islice(primes(), 10)))
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
itertools pipeline
#Using `itertools.chain`, `itertools.filterfalse`, and a generator expression, build a pipeline that: 1. Chains two lists together: `[1, 2, 3]` and `[4, 5, 6]`. 2. Filters out even numbers. 3. Squares the remaining numbers. Print the result as a list.
import itertools a = [1, 2, 3] b = [4, 5, 6] # Build the pipeline using itertools.chain, itertools.filterfalse, # and a generator expression for squaring. result = [] print(result) # [1, 9, 25]
Solution
import itertools
a = [1, 2, 3]
b = [4, 5, 6]
chained = itertools.chain(a, b) # 1 2 3 4 5 6
odds = itertools.filterfalse(lambda x: x % 2 == 0, chained) # 1 3 5
result = list(x * x for x in odds) # 1 9 25
print(result) # [1, 9, 25]
Group words by first letter with itertools.groupby
#Given a sorted list of words, use `itertools.groupby` to group them by their first letter and print each group on one line: `'a: apple, avocado'`. The list is already sorted alphabetically.
import itertools words = ['apple', 'avocado', 'banana', 'blueberry', 'cherry', 'coconut'] # Use itertools.groupby to group by first letter # Expected output: # a: apple, avocado # b: banana, blueberry # c: cherry, coconut
Solution
import itertools
words = ['apple', 'avocado', 'banana', 'blueberry', 'cherry', 'coconut']
for letter, group in itertools.groupby(words, key=lambda w: w[0]):
print(f'{letter}: {", ".join(group)}')