Python iterators and generators exercises yield iter solutions cheat sheet FP2

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.


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) from ArithmeticSequenceIterator (iterator)
  • __len__: count terms where start + n * diff < limit (or > limit if diff < 0)
  • __contains__: (value - start) % difference == 0 and value is within bounds
  • __reversed__: return iterator starting from last term, step = -difference
  • Infinite sequences (no limit): __len__ raises TypeError, __reversed__ raises TypeError

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 += base
  • squares_of(it): for x in it: yield x * x
  • running_sum(it): keep a total = 0 accumulator outside the loop
  • filter_even(it): for x in it: if x % 2 == 0: yield x
  • take(it, n): use a counter or range(n) with next()
  • 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._current and advances in __next__
  • __iter__ resets self._current = self._start and returns self
    — 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 a while loop
  • 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

Similar Posts

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *