Python iterators and generators exercises — master yield and iter
Python iterators and generators exercises are where yield and the iterator protocol become tools you reach for automatically. You’ve seen the theory and built three complete programs. Now it’s time to design your own — an arithmetic sequence iterator, a multiple generator and a number classifier.
As always: try to solve it yourself, use the hint if stuck for more than 10 minutes, and compare with the commented solution. Use pythontutor.com to step through and watch generators pause and resume at yield.
Table of Contents
Python iterators and generators exercises — Basic Level
Exercise 1 — Arithmetic sequence iterator
Implement an ArithmeticSequence class that models a mathematical arithmetic sequence — a sequence where each term differs from the previous by a constant value called the common difference.
Examples:
ArithmeticSequence(2, 3, 10) → 2, 5, 8 (start=2, diff=3, max=10) ArithmeticSequence(0, 2) → 0, 2, 4, 6, ... (infinite, diff=2) ArithmeticSequence(10, -1, 0) → 10, 9, 8, ..., 1 (countdown)
Requirements:
ArithmeticSequence(start, difference, limit=None) Must support: for x in seq: — iteration len(seq) — number of terms (only if limit set) x in seq — membership check reversed(seq) — reverse iteration (only if limit set) str(seq) — readable description take(n) — first n values as list sum() — sum of all terms (only if limit set) Formula: nth term: start + n * difference sum of n terms: n * (first + last) / 2
Expected output:
=== ARITHMETIC SEQUENCE === seq1 = ArithmeticSequence(1, 2, 10): 1, 3, 5, 7, 9 seq2 = ArithmeticSequence(0, 5): 0, 5, 10, 15, 20, ... len(seq1) = 5 sum(seq1) = 25 (formula: 5*(1+9)/2 = 25) 5 in seq1 = True 4 in seq1 = False reversed(seq1): 9, 7, 5, 3, 1 First 6 of seq2: [0, 5, 10, 15, 20, 25] for x in seq1: 1 3 5 7 9
💡 Hints:
- Separate
ArithmeticSequence(iterable) fromArithmeticSequenceIterator(iterator) __len__: count terms wherestart + n * diff < limit(or> limitif diff < 0)__contains__:(value - start) % difference == 0and value is within bounds__reversed__: return iterator starting from last term, step = -difference- Infinite sequences (no limit):
__len__raisesTypeError,__reversed__raisesTypeError
Python iterators and generators exercises — Intermediate Level
Exercise 2 — Multiple generator pipeline
Write a series of generator functions that can be composed into a pipeline. The goal is to process a sequence of numbers lazily — filtering, transforming and aggregating without building intermediate lists.
Required generators:
multiples(base, limit=None) → yields base, 2*base, 3*base, ... squares_of(iterable) → yields x² for each x in iterable filter_even(iterable) → yields only even values filter_odd(iterable) → yields only odd values running_sum(iterable) → yields cumulative sum take(iterable, n) → yields first n values
Then use them in pipelines:
# Pipeline 1: squares of multiples of 3, first 6 pipeline1 = take(squares_of(multiples(3)), 6) # Pipeline 2: running sum of even multiples of 2, first 8 pipeline2 = take(running_sum(filter_even(multiples(2))), 8) # Pipeline 3: odd squares of multiples of 5, first 5 pipeline3 = take(filter_odd(squares_of(multiples(5))), 5)
Expected output:
=== GENERATOR PIPELINES === Multiples of 3 (first 8): [3, 6, 9, 12, 15, 18, 21, 24] Pipeline 1 — squares of multiples of 3 (first 6): [9, 36, 81, 144, 225, 324] Pipeline 2 — running sum of even multiples of 2 (first 8): [2, 6, 12, 20, 30, 42, 56, 72] Pipeline 3 — odd squares of multiples of 5 (first 5): [25, 225, 625, 1225, 2025] Memory: each generator is ~104-200 bytes regardless of how many values it produces
💡 Hints:
multiples(base):n = base; while True: yield n; n += basesquares_of(it):for x in it: yield x * xrunning_sum(it): keep atotal = 0accumulator outside the loopfilter_even(it):for x in it: if x % 2 == 0: yield xtake(it, n): use a counter orrange(n)withnext()- Pipelines read right to left:
take(squares_of(multiples(3)), 6)— multiples feeds squares_of feeds take
Python iterators and generators exercises — Final Challenge
Exercise 3 — Number classifier iterator class
Implement a NumberClassifier class that takes a range of numbers and lazily classifies each one. It must implement the full iterator protocol as a class (not a generator function) and support multiple classification modes.
Requirements:
NumberClassifier(start, stop, mode='all')
Modes:
'all' → yields every number with its classification
'primes' → yields only prime numbers
'perfect' → yields only perfect numbers (sum of divisors = n)
'abundant' → yields abundant numbers (sum of divisors > n)
'deficient'→ yields deficient numbers (sum of divisors < n)
Each value yielded is a dict:
{'number': n, 'type': 'prime'/'perfect'/'abundant'/'deficient',
'divisors': [list of proper divisors], 'divisor_sum': total}
Must support:
for item in classifier: — full iteration
next(classifier) — manual next
len(classifier) — total numbers in range (not filtered)
classifier.count() — count of values that pass the filter
classifier.summary() — prints statistics
Expected output:
=== NUMBER CLASSIFIER === NumberClassifier(1, 30, 'primes'): 2 — prime 3 — prime 5 — prime 7 — prime 11 — prime 13 — prime 17 — prime 19 — prime 23 — prime 29 — prime Count: 10 NumberClassifier(1, 30, 'perfect'): 6 — perfect (divisors: [1, 2, 3], sum=6) 28 — perfect (divisors: [1, 2, 4, 7, 14], sum=28) Count: 2 NumberClassifier(1, 30, 'abundant'): 12, 18, 20, 24 ... (first 4 abundant numbers below 30) --- Summary for range 1-100 --- Total numbers: 100 Primes: 25 Perfect: 2 (6 and 28) Abundant: 22 Deficient: 71
💡 Hints:
divisors(n):[d for d in range(1, n) if n % d == 0]is_prime(n): no divisors between 2 and √n- The class stores
self._currentand advances in__next__ __iter__resetsself._current = self._startand returnsself
— OR — separate the classifier (iterable) from the iterator class- The mode filter is applied inside
__next__— skip values that don’t match the mode using awhileloop count(): iterate a fresh copy and count matches
Commented solutions
Solution Exercise 1
class ArithmeticSequenceIterator:
def __init__(self, start, diff, limit, reverse=False):
self._diff = diff
self._limit = limit
if reverse and limit is not None:
# Find the last term
if diff > 0:
n = (limit - start) // diff
while start + n * diff >= limit:
n -= 1
self._current = start + n * diff
self._end = start - 1
self._step = -diff
else:
n = 0
while start + (n+1) * diff > limit:
n += 1
self._current = start + n * diff
self._end = start + 1
self._step = -diff
else:
self._current = start
self._end = limit
self._step = diff
def __iter__(self):
return self
def __next__(self):
if self._end is not None:
if self._step > 0 and self._current >= self._end:
raise StopIteration
if self._step < 0 and self._current <= self._end:
raise StopIteration
value = self._current
self._current += self._step
return value
class ArithmeticSequence:
def __init__(self, start, difference, limit=None):
if difference == 0:
raise ValueError('Common difference cannot be zero')
self._start = start
self._diff = difference
self._limit = limit
@property
def is_infinite(self):
return self._limit is None
def _count_terms(self):
if self._limit is None:
return None
if self._diff > 0:
count = 0
current = self._start
while current < self._limit:
count += 1
current += self._diff
return count
else:
count = 0
current = self._start
while current > self._limit:
count += 1
current += self._diff
return count
def __iter__(self):
return ArithmeticSequenceIterator(
self._start, self._diff, self._limit
)
def __reversed__(self):
if self._limit is None:
raise TypeError('Cannot reverse infinite sequence')
return ArithmeticSequenceIterator(
self._start, self._diff, self._limit, reverse=True
)
def __len__(self):
if self._limit is None:
raise TypeError('Infinite sequence has no length')
return self._count_terms()
def __contains__(self, value):
if (value - self._start) % self._diff != 0:
return False
if self._limit is None:
if self._diff > 0:
return value >= self._start
else:
return value <= self._start
if self._diff > 0:
return self._start <= value < self._limit
else:
return self._limit < value <= self._start
def __bool__(self):
return self._limit is None or self._count_terms() > 0
def __str__(self):
terms = self.take(4)
preview = ', '.join(map(str, terms))
if self._limit is None:
return f'ArithmeticSequence({preview}, ...)'
if len(self) > 4:
return f'ArithmeticSequence({preview}, ..., limit={self._limit})'
all_terms = list(self)
return f'ArithmeticSequence({", ".join(map(str, all_terms))})'
def take(self, n):
result = []
for i, value in enumerate(self):
if i >= n:
break
result.append(value)
return result
def sum(self):
if self._limit is None:
raise TypeError('Cannot sum infinite sequence')
n = len(self)
if n == 0:
return 0
first = self._start
last = first + (n - 1) * self._diff
return n * (first + last) // 2
# Demo
print('=== ARITHMETIC SEQUENCE ===\n')
seq1 = ArithmeticSequence(1, 2, 10) # odd numbers: 1,3,5,7,9
seq2 = ArithmeticSequence(0, 5) # multiples of 5, infinite
print(f'seq1 = {seq1}')
print(f'seq2 = {seq2}')
print(f'\nlen(seq1) = {len(seq1)}')
print(f'sum(seq1) = {sum(seq1)} (formula: {len(seq1)}*(1+9)/2 = {len(seq1)*(1+9)//2})')
print(f'5 in seq1 = {5 in seq1}')
print(f'4 in seq1 = {4 in seq1}')
print(f'\nreversed(seq1):', end=' ')
for x in reversed(seq1):
print(x, end=' ')
print()
print(f'\nFirst 6 of seq2: {seq2.take(6)}')
print(f'\nfor x in seq1:')
print(' ', end=' ')
for x in seq1:
print(x, end=' ')
print()
# Multiple iterations work
print('\nIterating seq1 twice:')
for _ in range(2):
print(f' {list(seq1)}')
Solution Exercise 2
import sys
def multiples(base, limit=None):
"""Yields base, 2*base, 3*base, ..."""
n = base
while limit is None or n <= limit:
yield n
n += base
def squares_of(iterable):
"""Yields x² for each x in iterable."""
for x in iterable:
yield x * x
def filter_even(iterable):
"""Yields only even values."""
for x in iterable:
if x % 2 == 0:
yield x
def filter_odd(iterable):
"""Yields only odd values."""
for x in iterable:
if x % 2 != 0:
yield x
def running_sum(iterable):
"""Yields cumulative sum."""
total = 0
for x in iterable:
total += x
yield total
def take(iterable, n):
"""Yields first n values."""
count = 0
for x in iterable:
if count >= n:
break
yield x
count += 1
# Demo
print('=== GENERATOR PIPELINES ===\n')
print('Multiples of 3 (first 8):')
print(f' {list(take(multiples(3), 8))}')
print('\nPipeline 1 — squares of multiples of 3 (first 6):')
pipeline1 = take(squares_of(multiples(3)), 6)
print(f' {list(pipeline1)}')
print('\nPipeline 2 — running sum of even multiples of 2 (first 8):')
pipeline2 = take(running_sum(filter_even(multiples(2))), 8)
print(f' {list(pipeline2)}')
print('\nPipeline 3 — odd squares of multiples of 5 (first 5):')
pipeline3 = take(filter_odd(squares_of(multiples(5))), 5)
print(f' {list(pipeline3)}')
# Memory
gen = multiples(3)
print(f'\nMemory: generator object = {sys.getsizeof(gen)} bytes')
print('(regardless of how many values it will produce)')
Solution Exercise 3
import math
def divisors(n):
"""Return list of proper divisors of n."""
if n <= 1:
return []
divs = [1]
for d in range(2, int(math.sqrt(n)) + 1):
if n % d == 0:
divs.append(d)
if d != n // d:
divs.append(n // d)
return sorted(divs)
def classify_number(n):
"""Classify n and return a dict with its properties."""
divs = divisors(n)
div_sum = sum(divs)
if n < 2:
num_type = 'trivial'
elif all(n % d != 0 for d in range(2, int(math.sqrt(n)) + 1)):
num_type = 'prime'
elif div_sum == n:
num_type = 'perfect'
elif div_sum > n:
num_type = 'abundant'
else:
num_type = 'deficient'
return {
'number': n,
'type': num_type,
'divisors': divs,
'divisor_sum': div_sum
}
class NumberClassifierIterator:
def __init__(self, start, stop, mode):
self._current = start
self._stop = stop
self._mode = mode
def __iter__(self):
return self
def __next__(self):
while self._current < self._stop:
n = self._current
self._current += 1
info = classify_number(n)
if self._mode == 'all' or info['type'] == self._mode:
return info
raise StopIteration
class NumberClassifier:
VALID_MODES = {'all', 'primes', 'perfect', 'abundant', 'deficient'}
def __init__(self, start, stop, mode='all'):
if mode not in self.VALID_MODES:
raise ValueError(f'Invalid mode: {mode}. '
f'Choose from: {self.VALID_MODES}')
self._start = start
self._stop = stop
self._mode = mode
def __iter__(self):
return NumberClassifierIterator(self._start, self._stop, self._mode)
def __len__(self):
return self._stop - self._start
def count(self):
return sum(1 for _ in self)
def summary(self):
total = len(self)
counts = {mode: 0 for mode in ['prime', 'perfect', 'abundant', 'deficient']}
for info in NumberClassifier(self._start, self._stop, 'all'):
if info['type'] in counts:
counts[info['type']] += 1
print(f'\n--- Summary for range {self._start}-{self._stop} ---')
print(f'Total numbers: {total}')
for label, count in counts.items():
print(f'{label.capitalize():11}: {count}')
# Demo
print('=== NUMBER CLASSIFIER ===\n')
# Primes
print('NumberClassifier(1, 30, "primes"):')
primes_clf = NumberClassifier(1, 30, 'primes')
for item in primes_clf:
print(f' {item["number"]} — prime')
print(f'Count: {primes_clf.count()}')
# Perfect numbers
print('\nNumberClassifier(1, 30, "perfect"):')
for item in NumberClassifier(1, 30, 'perfect'):
print(f' {item["number"]} — perfect '
f'(divisors: {item["divisors"]}, sum={item["divisor_sum"]})')
print(f'Count: {NumberClassifier(1, 30, "perfect").count()}')
# Abundant numbers
print('\nNumberClassifier(1, 30, "abundant") — first 4:')
abundant = []
for item in NumberClassifier(1, 30, 'abundant'):
abundant.append(item['number'])
print(f' {abundant}')
# Summary
clf = NumberClassifier(1, 101, 'all')
clf.summary()
# Manual iteration
print('\nManual iteration:')
it = NumberClassifier(1, 6, 'all')
for item in it:
print(f' {item["number"]}: {item["type"]} '
f'(divisors: {item["divisors"]})')
Visualise with Python Tutor
Copy this code into pythontutor.com and step through it:
def multiples(base):
n = base
while True:
yield n
n += base
def squares_of(it):
for x in it:
yield x * x
def take(it, n):
count = 0
for x in it:
if count >= n:
break
yield x
count += 1
# Pipeline: squares of multiples of 3, first 4
result = list(take(squares_of(multiples(3)), 4))
print(result) # [9, 36, 81, 144]
# Manual step through
gen = take(squares_of(multiples(3)), 3)
print(next(gen)) # 9
print(next(gen)) # 36
print(next(gen)) # 81
Step through and observe the pipeline in action. When next(gen) is called on the take generator, it calls next() on squares_of, which calls next() on multiples. The chain activates from right to left — multiples yields 3, squares_of yields 9, take yields 9. On the second next(gen), the chain activates again — multiples resumes and yields 6, squares_of yields 36, take yields 36. Each generator is independently paused at its own yield — they’re a lazy pipeline where no value is computed until the outermost consumer asks for it.
Cheat sheet — Python iterators and generators
# ============================================
# CHEAT SHEET — Iterators and Generators
# Sergio Learns · sergiolearns.com
# ============================================
# ITERABLE vs ITERATOR
# Iterable: has __iter__ — returns fresh iterator each time
# Iterator: has __iter__ (returns self) + __next__
# remembers position, raises StopIteration when done
# BUILT-IN FUNCTIONS
iter(obj) # obj.__iter__() → iterator
next(it) # it.__next__() → next value
next(it, default) # returns default instead of StopIteration
# THE FOR LOOP
for x in obj: body
# Is:
_it = iter(obj)
while True:
try: x = next(_it); body
except StopIteration: break
# IMPLEMENTING AN ITERATOR (class)
class MyIterator:
def __init__(self, start, stop):
self._current = start
self._stop = stop
def __iter__(self): # always returns self
return self
def __next__(self):
if self._current >= self._stop:
raise StopIteration
value = self._current
self._current += 1
return value
# SEPARATING ITERABLE FROM ITERATOR (better design)
class MyIterable:
def __iter__(self):
return MyIterator(self._start, self._stop)
# → can iterate multiple times (each for gets fresh iterator)
# GENERATOR FUNCTION (easiest)
def counter(start, stop):
while start < stop:
yield start # pause + return value
start += 1 # resume here on next next()
# StopIteration raised automatically when function ends
# GENERATOR EXPRESSION
gen = (x**2 for x in range(10)) # lazy — () not []
# yield from — delegate to sub-iterable
def chain(*iters):
for it in iters:
yield from it # yields each item from it
# MEMORY
import sys
lst = [x for x in range(1_000_000)] # ~8.7 MB
gen = (x for x in range(1_000_000)) # ~104 bytes always
# INFINITE GENERATORS
def naturals(n=1):
while True:
yield n
n += 1
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Use with take() or itertools.islice()
def take(gen, n):
for _ in range(n): yield next(gen)
# GENERATOR PATTERNS
# Filter: for x in it: if condition(x): yield x
# Transform: for x in it: yield f(x)
# Accumulate: total=0; for x in it: total+=x; yield total
# Sliding win: for i in range(len-size+1): yield items[i:i+size]
# EXHAUSTION — generators are single-use!
gen = (x for x in range(3))
list(gen) # [0, 1, 2]
list(gen) # [] — exhausted!
# Unlike list: iter(lst) always creates fresh iterator
# KEY RULES
# 1. Iterator returns self from __iter__
# 2. Iterator raises StopIteration in __next__ when done
# 3. Iterable creates fresh iterator in __iter__
# 4. Generator pauses at yield, resumes on next()
# 5. Generator exhausted = StopIteration = loop ends
# 6. () for generator expression, [] for list comprehension
# 7. yield from delegates to sub-iterable item by item

One Comment