Python iterators and generators enable memory-efficient programming through lazy evaluation, producing items only when needed instead of loading everything into memory. They are ideal for handling large datasets, processing files, and building scalable data pipelines.
This page includes 30 Python iterator and generator exercises, from beginner to expert level, covering topics from the yield keyword to advanced coroutines and itertools, helping you write more efficient and Pythonic code.
Each coding challenge includes a Practice Problem, Hint, Solution code, and detailed Explanation, ensuring you don’t just copy code, but genuinely practice and understand how and why it works.
- All solutions have been fully tested on Python 3.
- Use our Online Code Editor to solve these exercises in real time.
+ Table of Contents (30 Exercises)
Table of contents
- Exercise 1: The Square Generator
- Exercise 2: Even Number Iterator
- Exercise 3: Custom Range
- Exercise 4: Reverse String Iterator
- Exercise 5: Vowel Filter
- Exercise 6: Power of Two
- Exercise 7: Finite Fibonacci
- Exercise 8: Infinite Counter
- Exercise 9: Manual Iteration
- Exercise 10: Step Iterator
- Exercise 11: File Line Reader
- Exercise 12: CSV Row Parser
- Exercise 13: The Pipeline
- Exercise 14: List Flattener
- Exercise 15: Batch Processing
- Exercise 16: Prime Sieve
- Exercise 17: Running Average
- Exercise 18: Sliding Window
- Exercise 19: Log Filter
- Exercise 20: Unique Element Filter
- Exercise 21: The Accumulator (send)
- Exercise 22: Custom Zip
- Exercise 23: Binary Tree Traversal
- Exercise 24: Data Throttler
- Exercise 25: Generator State Machine
- Exercise 26: Peekable Iterator
- Exercise 27: Exception Handler
- Exercise 28: Recursive Directory Walker
- Exercise 29: Interleaved Streams
- Exercise 30: Generator Cleanup
Exercise 1: The Square Generator
Problem Statement: Create a generator function that yields the squares of numbers from 1 to n.
Purpose: This exercise introduces generator functions and the yield keyword. Generators allow you to produce values lazily, one at a time, making them memory-efficient for large sequences – a key concept in Python’s iterator protocol.
Given Input: n = 5
Expected Output: 1 4 9 16 25
▼ Hint
- Define a function using the
defkeyword, and useyieldinstead ofreturnto produce values one at a time. - Use
range(1, n + 1)to iterate from 1 through n inclusive. - Inside the loop, yield
i ** 2ori * ito produce each square. - Call the generator in a for loop or pass it to
print()with*unpacking to see all values.
▼ Solution & Explanation
Explanation:
def square_generator(n): Defines a generator function. The presence ofyieldanywhere in the body makes it a generator rather than a regular function.yield i ** 2: Pauses execution and sends the squared value to the caller. On the next iteration, execution resumes from the line afteryield.range(1, n + 1): Generates integers from 1 to n inclusive. Without+ 1, n itself would be excluded.end=" ": Passed toprint()to separate values with a space instead of a newline, keeping all output on one line.
Exercise 2: Even Number Iterator
Problem Statement: Write a class-based iterator that returns even numbers up to a specified limit.
Purpose: This exercise teaches you how to implement the iterator protocol manually using __iter__ and __next__ methods. Understanding class-based iterators gives you full control over state and iteration logic.
Given Input: limit = 10
Expected Output: 0 2 4 6 8 10
▼ Hint
- Create a class with an
__init__method that stores the limit and initializes acurrentcounter to 0. - Implement
__iter__to returnself– this makes the object itself the iterator. - Implement
__next__to return the current value and advance by 2. RaiseStopIterationwhen the current value exceeds the limit. - Use a
forloop to iterate over an instance of the class – Python calls__next__automatically untilStopIterationis raised.
▼ Solution & Explanation
Explanation:
__init__(self, limit): Sets up the iterator’s state.self.currenttracks the next value to be returned, starting at 0.__iter__(self): Returnsself, which satisfies Python’s requirement that an iterable object return an iterator wheniter()is called on it.__next__(self): Returns the current even number and advancesself.currentby 2. This is called automatically byforloops and other iteration constructs.raise StopIteration: Signals to theforloop that there are no more values, causing iteration to end cleanly.
Exercise 3: Custom Range
Problem Statement: Re-implement a simplified version of the range() function using a generator.
Purpose: This exercise deepens your understanding of how Python’s built-in range() works under the hood. Rebuilding familiar tools from scratch is a powerful way to solidify your grasp of generators and lazy evaluation.
Given Input: start = 2, stop = 10, step = 2
Expected Output: 2 4 6 8
▼ Hint
- Define a generator function with parameters
start,stop, andstep=1(default step of 1). - Use a
whileloop with the conditioncurrent < stopto keep yielding values. - Inside the loop,
yieldthe current value, then increment it bystep. - Note that like the built-in
range(), the stop value should be excluded from the output.
▼ Solution & Explanation
Explanation:
def custom_range(start, stop, step=1): Defines the generator with a default step of 1, mirroring the signature of Python’s built-inrange().current = start: Initializes the running counter outside the loop so it persists acrossyieldcalls.while current < stop: Continues yielding as long as the current value has not reached the stop boundary, which is excluded – consistent with built-inrange()behaviour.current += step: Advances the counter after each yield. Placing this afteryieldensures the current value is produced before being incremented.
Exercise 4: Reverse String Iterator
Problem Statement: Write an iterator class that takes a string and returns its characters in reverse order.
Purpose: This exercise combines class-based iterator design with string indexing. It reinforces how iterators maintain state between calls and how negative indexing works in Python sequences.
Given Input: text = "hello"
Expected Output: o l l e h
▼ Hint
- In
__init__, store the string and set an index starting atlen(string) - 1to begin from the last character. - In
__next__, check if the index is less than 0. If so, raiseStopIteration. - Return the character at the current index, then decrement the index by 1.
- Remember to implement
__iter__returningselfso the class works inside aforloop.
▼ Solution & Explanation
Explanation:
self.index = len(text) - 1: Sets the starting position to the last valid index of the string. For"hello"(length 5), this is index 4, pointing to'o'.if self.index < 0: Once the index goes below zero, all characters have been yielded. RaisingStopIterationcleanly ends the loop.char = self.text[self.index]: Retrieves the character at the current position before decrementing, ensuring the correct character is returned.self.index -= 1: Moves the pointer one step toward the beginning of the string on each call to__next__.
Exercise 5: Vowel Filter
Problem Statement: Create a generator that takes a string and yields only the vowels found in it.
Purpose: This exercise shows how generators can act as filters, processing a stream of data and selectively yielding items that meet a condition. This pattern is widely used in data pipelines and text processing.
Given Input: text = "Hello, World!"
Expected Output: e o o
▼ Hint
- Define a generator function that accepts a string parameter.
- Use a
forloop to iterate over each character in the string. - Check membership using
char.lower() in "aeiou"to handle both uppercase and lowercase vowels. - Use
yieldinside theifblock to produce only the matching characters.
▼ Solution & Explanation
Explanation:
vowels = "aeiou": Stores all lowercase vowels in a string. Using a string for membership testing withinis concise and readable for small character sets.char.lower() in vowels: Converts the character to lowercase before checking, so both'H'and'h'are handled correctly without needing to list uppercase vowels separately.yield char: Produces only the characters that pass the filter condition. Non-vowel characters are silently skipped, demonstrating the generator-as-filter pattern.- Generator advantage: Because this is a generator, it processes the string one character at a time without building an intermediate list – making it efficient for very long strings or streamed input.
Exercise 6: Power of Two
Problem Statement: Write a generator expression (one-liner) that yields powers of 2 (e.g., 1, 2, 4, 8…).
Purpose: This exercise introduces generator expressions as a compact alternative to generator functions. They follow the same lazy evaluation model but use a concise syntax similar to list comprehensions, making them ideal for simple transformations.
Given Input: n = 8
Expected Output: 1 2 4 8 16 32 64 128
▼ Hint
- A generator expression uses parentheses instead of square brackets:
(expr for x in iterable). It is lazy, unlike a list comprehension. - Use
2 ** ias the expression andrange(n)as the iterable, whereigoes from 0 to n – 1. - Assign the generator expression to a variable, then iterate over it with a
forloop to print each value.
▼ Solution & Explanation
Explanation:
(2 ** i for i in range(n)): A generator expression enclosed in parentheses. It behaves like a generator function withyield, but is written as a single expression. No list is built in memory.range(n): Produces exponents 0 through n – 1. Since2 ** 0 = 1, the sequence correctly starts at 1.powers: Holds a generator object. The values are not computed until theforloop requests them, one at a time.- Generator vs. list comprehension: Replacing the outer parentheses with square brackets would produce a list immediately. The generator version defers computation, which matters when
nis very large.
Exercise 7: Finite Fibonacci
Problem Statement: Write a generator to produce the first n numbers in the Fibonacci sequence, where each number is the sum of the two preceding ones.
Purpose: This exercise demonstrates how generators can maintain multiple pieces of state across yields. The Fibonacci sequence is a classic example where generating values on demand is more elegant than building the full list upfront.
Given Input: n = 8
Expected Output: 0 1 1 2 3 5 8 13
▼ Hint
- Initialize two variables
a, b = 0, 1before the loop. On each iteration, yielda, then update both:a, b = b, a + b. - Use
for _ in range(n)to control how many values are produced, where_signals the loop variable is unused. - The simultaneous assignment
a, b = b, a + bavoids needing a temporary variable – Python evaluates the right-hand side fully before assigning.
▼ Solution & Explanation
Explanation:
a, b = 0, 1: Seeds the sequence with the first two Fibonacci values. Both variables persist in the generator’s local scope across everyyield.yield a: Produces the current Fibonacci number and pauses execution. The next call to the generator resumes on the line immediately after this statement.a, b = b, a + b: Advances the sequence in one step. Python evaluatesbanda + bsimultaneously before assignment, so the old value ofais used in computing the newb.for _ in range(n): Limits output to exactlynvalues. The underscore is a convention indicating the loop variable itself is not needed inside the body.
Exercise 8: Infinite Counter
Problem Statement: Create a generator that starts at 1 and counts up infinitely (don’t run this without a break condition!).
Purpose: This exercise shows that generators are not required to terminate. Infinite generators are a powerful pattern in Python, commonly used alongside itertools or consumed with a break once enough values have been produced.
Given Input: Print values until the counter reaches 5.
Expected Output: 1 2 3 4 5
▼ Hint
- Use a
while Trueloop inside the generator to yield values indefinitely. Increment the counter after eachyield. - In the consuming for loop, use an
ifstatement withbreakto stop once the desired limit is reached.
▼ Solution & Explanation
Explanation:
while True: Creates an unbounded loop inside the generator. Because execution pauses atyieldeach time, this does not cause an infinite loop in the traditional sense – the generator only advances when the caller requests the next value.yield current: Suspends the generator and hands the current count to the caller. Thewhile Trueloop resumes only whennext()is called again.if num > 5: break: Exits theforloop and closes the generator. Without a guard like this, the loop would run indefinitely and the program would never terminate.start=1: A default parameter makes the generator flexible. You can start counting from any number by passing a different value, e.g.infinite_counter(10).
Exercise 9: Manual Iteration
Problem Statement: Given a list [10, 20, 30], use the iter() and next() functions to print each element manually, catching the StopIteration exception.
Purpose: This exercise pulls back the curtain on what a for loop does internally. Understanding iter() and next() directly gives you precise control over iteration and helps you reason about how Python’s iterator protocol works at a low level.
Given Input: numbers = [10, 20, 30]
Expected Output: 10 20 30
▼ Hint
- Call
iter(numbers)to get an iterator object from the list, then callnext()on it repeatedly to retrieve each element one by one. - Wrap each
next()call in atry/except StopIterationblock, or use awhile Trueloop and break inside theexceptclause when the list is exhausted.
▼ Solution & Explanation
Explanation:
iter(numbers): Callsnumbers.__iter__()internally and returns an iterator object. Lists are iterable but not iterators themselves – this step produces the iterator that tracks position.next(iterator): Callsiterator.__next__()and returns the next value. Each call advances the internal pointer by one position.except StopIteration: break: When the list is exhausted,next()raisesStopIteration. Catching it here exits the loop cleanly, which is exactly what aforloop does under the hood.- What a
forloop really does: The patterniter()then repeatednext()untilStopIterationis precisely how Python implements everyforloop. Writing it manually makes that mechanism explicit.
Exercise 10: Step Iterator
Problem Statement: Create an iterator that takes a start, stop, and step, returning values just like range() but allowing for float steps.
Purpose: This exercise extends the custom range concept from Exercise 3 to support floating-point steps – something Python’s built-in range() deliberately excludes. It reinforces class-based iterator design while highlighting a practical limitation of the standard library.
Given Input: start = 0.0, stop = 1.0, step = 0.25
Expected Output: 0.0 0.25 0.5 0.75
▼ Hint
- Build a class with
__init__,__iter__, and__next__. Storestart,stop, andstepin__init__, and usestartas the initial value of acurrentcounter. - In
__next__, raiseStopIterationwhencurrent >= stop, otherwise return the current value and advance bystep. Useround()to avoid floating-point accumulation errors.
▼ Solution & Explanation
Explanation:
self.current = start: Initializes the running position. Usingstartdirectly (rather than a separate index) makes float arithmetic straightforward and keeps the logic close to the custom range from Exercise 3.if self.current >= self.stop: Ends iteration when the current value reaches or passesstop, matching the exclusive upper bound of Python’s built-inrange().round(self.current + self.step, 10): Floating-point addition can accumulate tiny errors (e.g.0.1 + 0.2 = 0.30000000000000004). Rounding to 10 decimal places corrects this without affecting precision at normal step sizes.- Why not use
range()here: Python’srange()only accepts integers. This class fills that gap, and the same pattern can be extended to support negative float steps by adjusting the stop condition accordingly.
Exercise 11: File Line Reader
Problem Statement: Write a generator that reads a large text file line-by-line to avoid loading the entire file into memory.
Purpose: This exercise demonstrates one of the most practical real-world uses of generators: memory-efficient file processing. Instead of reading all lines into a list at once, the generator yields one line at a time, keeping memory usage constant regardless of file size.
Given Input: A text file sample.txt containing three lines: Hello, World!, Python is great., Generators save memory.
Expected Output: Hello, World! Python is great. Generators save memory.
▼ Hint
- Open the file using a
withstatement inside the generator, then use aforloop over the file object – file objects in Python are themselves iterable, yielding one line at a time. - Call
line.rstrip("\n")before yielding to strip the trailing newline character from each line. - Create the sample file first using a
with open(..., "w")block so the generator has something to read.
▼ Solution & Explanation
Explanation:
with open(filepath, "r") as f: Opens the file inside the generator using a context manager. Thewithblock ensures the file is closed automatically, even if the caller stops iterating early.for line in f: File objects implement the iterator protocol natively. Python reads one line from disk per iteration, so only a single line occupies memory at any given moment.line.rstrip("\n"): Removes the trailing newline character that the file includes at the end of each line. Without this, each printed line would be followed by a blank line.- Memory advantage: Calling
f.readlines()would load every line into a list at once. This generator approach keeps memory usage flat – a 10 GB log file consumes no more memory than a 1 KB file.
Exercise 12: CSV Row Parser
Problem Statement: Create a generator that yields rows from a CSV file as dictionaries, using the first row as keys.
Purpose: This exercise combines file reading with dictionary construction to produce a practical data-ingestion pattern. Yielding rows as dictionaries makes downstream code more readable, and the generator keeps memory consumption low for large CSV files.
Given Input: A CSV file data.csv with the following content:
name,age,city Alice,30,New York Bob,25,London Carol,35,Sydney
Expected Output:
{'name': 'Alice', 'age': '30', 'city': 'New York'}
{'name': 'Bob', 'age': '25', 'city': 'London'}
{'name': 'Carol', 'age': '35', 'city': 'Sydney'}
▼ Hint
- Read the first line separately to extract the header keys using
next(f), then split it by comma. For each subsequent line, split by comma and usezip(headers, values)to pair keys with values. - Pass the zipped pairs to
dict()to build each row dictionary, thenyieldit. Use.strip()on values to remove any surrounding whitespace or newline characters.
▼ Solution & Explanation
Explanation:
next(f): Advances the file iterator by one line and returns it. Calling this before theforloop consumes the header row so subsequent iterations only see data rows.zip(headers, values): Pairs each header with the corresponding value by position, producing tuples such as('name', 'Alice'). Passing this todict()builds the row dictionary in one step..strip().split(","):strip()removes the trailing newline and any surrounding whitespace beforesplit()separates the fields. Skippingstrip()would leave"\n"attached to the last value of every row.- Note on the standard library: Python’s
csv.DictReaderhandles edge cases such as quoted commas and escaped characters. This manual implementation is intentionally simplified to illustrate the underlying pattern rather than replacecsv.DictReaderin production code.
Exercise 13: The Pipeline
Problem Statement: Create two generators: one that yields numbers and another that squares them. Pipe the first into the second.
Purpose: This exercise introduces generator pipelines, a composable pattern where the output of one generator feeds directly into the next. Pipelines are a cornerstone of data processing in Python, allowing complex transformations to be built from small, single-purpose generators.
Given Input: numbers = [1, 2, 3, 4, 5]
Expected Output: 1 4 9 16 25
▼ Hint
- Write a
number_producergenerator that yields each item from an iterable, and a separatesquarergenerator that accepts an iterable, iterates over it, and yields each value squared. - Chain them by passing the first generator directly as the argument to the second:
squarer(number_producer(numbers)). Neither generator runs until the finalforloop pulls values.
▼ Solution & Explanation
Explanation:
number_producer(iterable): A pass-through generator that yields each item from any iterable unchanged. In a real pipeline this stage would typically filter or transform the source data before passing it on.squarer(iterable): Accepts any iterable – including another generator – and yields the square of each value. Because it takes an iterable as input, it is decoupled from the source and can be reused with any producer.squarer(number_producer(numbers)): Composes the two generators without executing either. This is lazy composition: no computation happens until theforloop callsnext()onpipeline.- Pipeline scalability: Additional transformation stages can be added by wrapping further generators around
pipeline. Each stage processes one value at a time, so the total memory used stays constant regardless of how many stages exist.
Exercise 14: List Flattener
Problem Statement: Use yield from to create a recursive generator that flattens a nested list (e.g., [1, [2, 3], 4]).
Purpose: This exercise introduces yield from, a keyword that delegates generation to a sub-iterable. Combining it with recursion produces an elegant solution for traversing arbitrarily deep nested structures – a pattern that appears frequently in tree processing and JSON parsing.
Given Input: nested = [1, [2, [3, 4], 5], [6, 7], 8]
Expected Output: 1 2 3 4 5 6 7 8
▼ Hint
- Loop over each item in the input. If the item is a
list, useyield from flatten(item)to recurse into it. Otherwise,yieldthe item directly. yield fromtransparently forwards every value produced by the recursive call to the caller, so you do not need a nested loop to collect and re-yield results.
▼ Solution & Explanation
Explanation:
isinstance(item, list): Checks whether the current item is itself a list. If it is, the function recurses into it. If it is not, the item is a plain value and is yielded directly.yield from flatten(item): Delegates to a recursive generator call. Every value the inner call yields is passed straight through to the outermost caller, without needing a loop to collect and re-yield each one manually.- Arbitrary depth: Because each nested list triggers a new recursive call, the function handles any level of nesting automatically. A three-level nest such as
[1, [2, [3, 4]]]is flattened with the same code as a one-level nest. yield fromvs. a manual loop: Withoutyield from, you would needfor v in flatten(item): yield v. Theyield fromsyntax is not just shorter – it also correctly forwardssend()values and exceptions into the sub-generator, which the manual loop cannot do.
Exercise 15: Batch Processing
Problem Statement: Write a generator that takes an iterable and yields chunks of a specific size (e.g., process a list of 100 items in batches of 10).
Purpose: Batch processing is essential when working with APIs, databases, or large datasets where processing all items at once is impractical. This exercise shows how a generator can group an arbitrary stream of values into fixed-size chunks without loading the full dataset into memory.
Given Input: items = list(range(1, 11)), batch_size = 3
Expected Output:
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
[10]
▼ Hint
- Convert the iterable to a list, then use a
forloop withrange(0, len(items), batch_size)to step through it in increments ofbatch_size. - On each iteration, slice the list with
items[i : i + batch_size]andyieldthe slice. The last slice will naturally be shorter if the total count is not evenly divisible.
▼ Solution & Explanation
Explanation:
items = list(iterable): Materialises the iterable into a list so it can be sliced by index. This is necessary because generators and other one-shot iterables do not support slicing directly.range(0, len(items), batch_size): Produces starting indices 0, 3, 6, 9, … stepping bybatch_size. Each index marks the beginning of one chunk.items[i : i + batch_size]: Python slicing never raises anIndexErrorwhen the upper bound exceeds the list length. The final chunk ([10]in this example) is returned as a shorter slice automatically.- Practical use: In real applications, each yielded chunk might be sent as a single bulk API request, inserted as one database transaction, or written as one file segment – making the batch size easy to tune without changing the processing logic.
Exercise 16: Prime Sieve
Problem Statement: Implement a generator that yields prime numbers using the Sieve of Eratosthenes logic.
Purpose: This exercise shows how a classical algorithm can be expressed as a lazy generator. The sieve approach is significantly more efficient than trial division for finding all primes up to a limit, and mapping it to a generator makes the result easy to consume incrementally.
Given Input: limit = 30
Expected Output: 2 3 5 7 11 13 17 19 23 29
▼ Hint
- Create a boolean list
is_primeof lengthlimit + 1, initialised toTrue. Set indices 0 and 1 toFalse, then loop from 2 upward: when you find aTrueentry, mark all its multiples asFalseusing a slice assignment. - After the sieve is built, loop over the list and
yieldevery index whose value is stillTrue. The inner marking loop only needs to run whilei * i <= limit, since any composite number must have a factor at or below its square root. - Use the slice assignment
is_prime[i*i::i] = [False] * len(is_prime[i*i::i])to mark all multiples ofistarting fromi²in one step.
▼ Solution & Explanation
Explanation:
is_prime = [True] * (limit + 1): Allocates a boolean lookup table indexed by number. Every candidate starts as assumed prime; the sieve progressively marks composites asFalse.range(2, int(limit ** 0.5) + 1): The outer loop only needs to run up to the square root of the limit. Any composite number must have at least one factor at or below its square root, so all composites are guaranteed to be marked before this loop ends.is_prime[i * i :: i] = [False] * ...: Slice assignment marks every multiple ofistarting fromi²in a single operation. Smaller multiples such as2iand3iwere already marked by earlier primes, so starting ati²avoids redundant work.- Two-phase design: The sieve marks composites first, then a second loop yields surviving primes. The
yieldappears only in the second loop, so the generator produces output only after the full table is built – a deliberate trade-off of latency for correctness.
Exercise 17: Running Average
Problem Statement: Create a generator that maintains and yields the running average of a stream of numbers.
Purpose: This exercise shows how a generator can carry internal state between yields to perform stateful stream processing. A running average is a common operation in monitoring, analytics, and signal processing, and computing it lazily avoids storing the entire history of values.
Given Input: numbers = [10, 20, 30, 40, 50]
Expected Output: 10.0 15.0 20.0 25.0 30.0
▼ Hint
- Track two variables across iterations: a running
total(sum so far) and acount(number of values seen). Update both on each iteration, thenyield total / count. - Both variables are initialised to 0 before the loop and persist between
yieldcalls because they live in the generator’s local scope.
▼ Solution & Explanation
Explanation:
total = 0andcount = 0: Both accumulators are declared before the loop. Because they live in the generator’s local scope, their values survive acrossyieldcalls and are available on every subsequent iteration.total += valueandcount += 1: Updated before yielding so the average includes the current value. Swapping the order – yielding before updating – would produce a one-step-behind average.yield total / count: Python 3’s/operator always returns a float, so the output is10.0rather than10even for whole-number averages. This is consistent with the expected output and avoids surprises when the average is not a whole number.- Stateful processing without storage: Only two scalar variables are kept in memory regardless of how many numbers have been processed. Storing all values to recalculate the average from scratch each time would use O(n) memory and O(n) time per step.
Exercise 18: Sliding Window
Problem Statement: Write a generator that yields a sliding window of size n over a sequence (e.g., for [1, 2, 3, 4] with window size 2, it yields (1, 2), (2, 3), (3, 4)).
Purpose: Sliding windows appear in time-series analysis, text n-gram generation, and signal smoothing. This exercise demonstrates how a generator can maintain a small buffer of recent values and expose it as a structured output, combining slicing with lazy production.
Given Input: sequence = [1, 2, 3, 4, 5], n = 3
Expected Output: (1, 2, 3) (2, 3, 4) (3, 4, 5)
▼ Hint
- Convert the sequence to a list, then loop with
range(len(items) - n + 1). On each iteration, sliceitems[i : i + n]and yield it as a tuple. - The upper bound
len(items) - n + 1ensures the last window is exactly sizenand does not extend beyond the end of the sequence.
▼ Solution & Explanation
Explanation:
items = list(sequence): Materialises the input into a list so it can be accessed by index and sliced. Without this step, a one-shot iterable such as a generator passed assequencewould be exhausted after the first access.range(len(items) - n + 1): Calculates exactly how many full windows fit. For a 5-element list with window size 3, this givesrange(3), producing starting indices 0, 1, and 2 – corresponding to the three valid windows.tuple(items[i : i + n]): Slices out the window and converts it to a tuple. Tuples signal that the window is a fixed-size, ordered snapshot rather than a mutable collection, which is the conventional output type for windowed data.- Standard library alternative: From Python 3.12 onward,
itertools.sliding_window(iterable, n)provides the same behaviour. This implementation makes the underlying index arithmetic explicit and works on all Python 3 versions.
Exercise 19: Log Filter
Problem Statement: Create a generator that parses a log file and only yields lines containing the keyword ERROR.
Purpose: This exercise combines file reading with conditional filtering in a single generator, demonstrating a pattern central to log analysis and data pipelines. Yielding only matching lines keeps memory usage constant and makes the generator composable with downstream stages.
Given Input: A log file app.log with the following content:
INFO: Server started ERROR: Disk quota exceeded INFO: Request received ERROR: Database connection failed WARNING: High memory usage
Expected Output:
ERROR: Disk quota exceeded
ERROR: Database connection failed
▼ Hint
- Read the file line by line inside a
with open(...)block. For each line, callline.strip()to remove surrounding whitespace, then checkif "ERROR" in linebefore yielding. - This is the same file-reading pattern from Exercise 11, extended with a single conditional filter before the
yieldstatement.
▼ Solution & Explanation
Explanation:
keyword="ERROR": A default parameter makes the generator reusable for any keyword – passkeyword="WARNING"and the same function filters for warnings instead. No code changes are needed for different log levels.line = line.strip(): Removes both the trailing newline and any leading or trailing whitespace before the membership check and before yielding. Without this, printed output would include blank lines between entries.if keyword in line: A simple substring check. Only lines that contain the keyword are yielded; all others are silently skipped. This is the generator-as-filter pattern from Exercise 5 applied to file input.- Pipeline readiness: Because the generator yields individual strings, it can be piped directly into another generator – for example, one that extracts timestamps or counts occurrences – without any changes to this stage.
Exercise 20: Unique Element Filter
Problem Statement: Write a generator that takes an iterable with duplicates and yields only unique elements in the order they appear.
Purpose: Deduplication while preserving insertion order is a common requirement in data cleaning, URL deduplication, and event stream processing. This exercise shows how a generator can maintain a small auxiliary data structure to track state without exposing it to the caller.
Given Input: items = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
Expected Output: 3 1 4 5 9 2 6
▼ Hint
- Maintain a
seenset inside the generator. For each item, checkif item not in seen: if so, add it toseenandyieldit; otherwise skip it. - A set is used rather than a list because membership testing with
inis O(1) for sets and O(n) for lists, which matters when the iterable is large.
▼ Solution & Explanation
Explanation:
seen = set(): An empty set initialised before the loop. It persists across yields in the generator’s local scope, accumulating every element that has been produced so far.if item not in seen: Set membership testing runs in O(1) average time because sets use hash-based lookup. Checking the same condition against a list would take O(n) time per element, making the overall algorithm O(n²).seen.add(item)thenyield item: The item is recorded before being yielded. If the caller stopped iterating early (e.g., withbreak), any items added toseenup to that point would still correctly block duplicates if iteration resumed later vianext().- Order preservation: Unlike converting to a
setdirectly – which loses insertion order – this generator yields items in the order they first appear. From Python 3.7 onward,dict.fromkeys(iterable)also preserves order, but the generator approach works element by element without materialising any intermediate collection.
Exercise 21: The Accumulator (send)
Problem Statement: Create a generator (coroutine) that uses yield to receive values via .send() and keeps a running total.
Purpose: This exercise introduces the coroutine side of generators – the ability to push values into a generator using .send(), not just pull values out of it. Coroutines are the foundation of Python’s async/await syntax and are widely used for cooperative multitasking and data pipelines that need to accept input at each step.
Given Input: Send the values 10, 20, 30 one at a time.
Expected Output: 10 30 60
▼ Hint
- Write a generator with
total = 0before awhile Trueloop. Inside the loop, assignvalue = yield total: the right-hand side sends the current total out, and the left-hand side receives the next value sent in via.send(). Addvaluetototalafter each receive. - Before calling
.send()with a real value, you must first advance the generator to its firstyieldby callingnext(acc)oracc.send(None). Sending a non-Nonevalue to an unprimed generator raises aTypeError. - Each
.send(value)call resumes the generator, assigns the sent value tovalue, adds it tototal, then loops back toyield total, pausing again and returning the new running total to the caller.
▼ Solution & Explanation
Explanation:
value = yield total: A two-directional yield. The expression to the right ofyieldis sent out to the caller as the generator’s current output. The value sent back in via.send()is assigned tovalueon the left-hand side when the generator resumes.next(acc): Primes the coroutine by advancing it to the firstyield. At this pointtotalis 0, so the generator pauses and returns0. The return value is discarded here because we have not sent any data yet.acc.send(10): Resumes the generator and assigns10tovalue. The loop adds it tototal, makingtotal = 10, then reachesyield totalagain, pausing and returning10to the caller.if value is None: break: A clean shutdown guard. Callingacc.send(None)ornext(acc)after the coroutine is primed setsvaluetoNone, which exits the loop and allows the generator to terminate gracefully viaStopIteration.
Exercise 22: Custom Zip
Problem Statement: Re-implement the zip() function using iter() and next() to combine two iterables.
Purpose: This exercise deepens your understanding of the iterator protocol by rebuilding a familiar built-in from first principles. It also demonstrates how zip() stops at the shortest iterable and how StopIteration can be used as a natural exit signal from within a generator.
Given Input: a = [1, 2, 3], b = ["a", "b", "c"]
Expected Output: (1, 'a') (2, 'b') (3, 'c')
▼ Hint
- Call
iter()on both inputs to get two iterator objects. Inside awhile Trueloop, callnext()on each iterator in sequence and yield a tuple of both results. - Wrap the
next()calls in atry/except StopIterationblock andreturninside theexceptclause. When either iterator is exhausted,StopIterationis raised and the generator exits cleanly, mirroring built-inzip()behaviour.
▼ Solution & Explanation
Explanation:
iter_a = iter(iterable_a): Converts each input to a stateful iterator. This ensures the function accepts any iterable – lists, generators, strings, or custom classes – not just sequences that support indexing.try / except StopIteration: return: When either iterator runs out of values,next()raisesStopIteration. Catching it and usingreturninside a generator is the idiomatic way to end iteration early – Python converts thereturnstatement into aStopIterationthat the caller receives.yield (item_a, item_b): Produces one paired tuple per loop iteration. Theyieldis placed after bothnext()calls succeed, guaranteeing that a partial tuple is never emitted if the second iterator is shorter than the first.- Shortest-iterable behaviour: If
ahas 3 elements andbhas 2, the generator stops after 2 pairs – exactly like built-inzip(). For longest-iterable behaviour, Python offersitertools.zip_longest(), which fills missing values with a configurable default.
Exercise 23: Binary Tree Traversal
Problem Statement: Implement an in-order traversal of a Binary Search Tree using a recursive generator.
Purpose: This exercise applies yield from to a recursive data structure, showing how generators can traverse trees without the caller needing to manage a stack or collect results into a list first. The pattern generalises to any recursive structure such as file system trees or JSON documents.
Given Input: A BST built by inserting values in this order: 5, 3, 7, 2, 4, 6, 8
Expected Output: 2 3 4 5 6 7 8
▼ Hint
- Define a
Nodeclass withvalue,left, andrightattributes. Build the BST with aninsertmethod that places smaller values to the left and larger values to the right recursively. - In the traversal generator, follow the in-order pattern:
yield from inorder(node.left), thenyield node.value, thenyield from inorder(node.right). Guard each recursive call withif node is not None– or check at the top of the function and return immediately if the node isNone.
▼ Solution & Explanation
Explanation:
if node is None: return: The base case of the recursion. A plainreturninside a generator raisesStopIterationsilently, so the caller never sees theNoneleaves – iteration simply ends for that branch.yield from inorder(node.left): Delegates all values from the left subtree directly to the outermost caller. Withoutyield from, a manual loop would be needed to re-yield each value, andsend()andthrow()semantics would be lost.yield node.value: Produces the current node’s value between the two recursive calls. In-order traversal visits left subtree first, then the node itself, then the right subtree – which for a BST always produces values in ascending sorted order.- Why a generator, not a list: A list-based approach collects all values before returning, requiring O(n) extra memory. The generator yields each value as the recursion unwinds, meaning only the call stack depth (O(log n) for a balanced BST) is needed beyond the tree itself.
Exercise 24: Data Throttler
Problem Statement: Create a generator that wraps another iterable but only yields every n-th item.
Purpose: Downsampling a stream by taking every n-th element is a common technique in signal processing, data visualisation, and performance testing. This exercise shows how a thin generator wrapper can add filtering logic to any existing iterable without modifying it.
Given Input: data = list(range(1, 21)), n = 4
Expected Output: 4 8 12 16 20
▼ Hint
- Use
enumerate(iterable, start=1)to pair each item with a 1-based index. Inside the loop, yield the item only whenindex % n == 0. - Starting
enumerateat 1 (rather than the default 0) ensures the first yielded item is the n-th element of the input, not the (n-1)-th.
▼ Solution & Explanation
Explanation:
enumerate(iterable, start=1): Pairs each item with a 1-based counter. Usingstart=1means the first item has index 1, so the modulo checkindex % n == 0correctly identifies the 4th, 8th, 12th, … elements rather than the 3rd, 7th, 11th.index % n == 0: The modulo operator returns the remainder of dividingindexbyn. A remainder of zero means the current position is an exact multiple ofn, making it a keep position.- Composability: Because
throttleaccepts any iterable, it can wrap a live sensor feed, a file reader, or another generator just as easily as a list. The upstream iterable is never fully materialised, keeping memory usage constant. - Standard library alternative:
itertools.islice(iterable, n-1, None, n)achieves the same result. The custom generator is more readable for this specific case and avoids importingitertools, butisliceis preferable when working within a broaderitertoolspipeline.
Exercise 25: Generator State Machine
Problem Statement: Use a generator to represent a simple state machine (e.g., a Traffic Light that cycles between Green, Yellow, and Red).
Purpose: State machines model systems where behaviour depends on the current state and transitions follow defined rules. Encoding one as a generator is elegant because yield naturally represents a pause between state transitions, and the generator’s internal scope holds the current state without any external variables.
Given Input: Advance the traffic light through 6 transitions.
Expected Output: Green Yellow Red Green Yellow Red
▼ Hint
- Store the states in a list or tuple such as
["Green", "Yellow", "Red"]. Use awhile Trueloop with an innerforloop that iterates over the states and yields each one in turn. - In the consuming code, call
next()on the generator or iterate with aforloop combined withbreakoritertools.isliceto stop after a fixed number of transitions.
▼ Solution & Explanation
Explanation:
states = ["Green", "Yellow", "Red"]: The ordered list of states defines the transition sequence. Changing this list is all that is needed to modify the machine’s behaviour – no logic elsewhere needs to change.while Truewrappingfor state in states: The innerforloop cycles through all states once; the outerwhile Truerestarts it immediately after the last state, producing an endless cycle. The generator never reaches a natural end.yield state: Pauses the generator and hands the current state to the caller. Execution resumes from this exact point on the nextnext()call, advancing to the following state in the sequence.for _ in range(6): next(light): Pulls exactly 6 states from the infinite generator. The caller controls how many transitions occur; the generator itself has no knowledge of when it will be stopped, which is the clean separation of concerns that makes this pattern reusable.
Exercise 26: Peekable Iterator
Problem Statement: Wrap an iterator in a class that allows you to “peek” at the next value without actually consuming it.
Purpose: Peeking is a fundamental technique in parsers, lexers, and lookahead algorithms, where a decision about the current token depends on what comes next. This exercise teaches you how to extend the standard iterator protocol with additional capabilities by wrapping an existing iterator in a class.
Given Input: data = [10, 20, 30, 40]
Expected Output:
Peek: 10
Consumed: 10
Peek: 20
Consumed: 20
Consumed: 30
Consumed: 40
▼ Hint
- In
__init__, calliter()on the input and eagerly fetch the first value into a_nextattribute usingnext(). Use a sentinel such as a uniqueobject()instance to distinguish “no value cached” from a cached value ofNone. - Implement a
peek()method that returns_nextwithout advancing the iterator. In__next__, return the cached_nextvalue and immediately fetch the following value from the underlying iterator into_nextto keep it primed. RaiseStopIterationwhen the underlying iterator is exhausted.
▼ Solution & Explanation
Explanation:
_SENTINEL = object(): A module-level sentinel is a unique object that can never appear as a legitimate value in the data stream. Comparing withis _SENTINELis safe even when the iterable containsNone,0, orFalse, which would all cause a naiveNone-based sentinel to mis-fire.self._next = next(self._iterator, _SENTINEL): The two-argument form ofnext()returns the default value instead of raisingStopIterationwhen the iterator is exhausted. This keeps the caching logic inside__init__and__next__free of try/except blocks.peek(): Returnsself._nextwithout advancing the iterator. Callingpeek()any number of times between twonext()calls always returns the same value, because nothing is consumed.__next__re-primes immediately: After saving the current_nextintovalue, the method fetches the following item from the underlying iterator before returning. This ensures_nextalways holds the upcoming value sopeek()is always accurate.
Exercise 27: Exception Handler
Problem Statement: Create a generator that uses .throw() to handle specific errors passed into it during execution.
Purpose: The .throw() method lets the caller inject an exception into a paused generator at the point of its current yield. This enables cooperative error handling between a generator and its driver, a pattern used in async frameworks and testing tools to simulate failure conditions mid-stream.
Given Input: A sequence of values 1, 2, 3, with a ValueError injected after the second value.
Expected Output:
Yielded: 1
Yielded: 2
Caught ValueError: simulated error - skipping
Yielded: 3
Generator finished
▼ Hint
- Wrap the
yieldstatement inside atry/except ValueErrorblock inside the generator. When.throw(ValueError, "message")is called from outside, the exception is raised at theyieldpoint and caught by theexceptclause, allowing the generator to resume normally. - In the driver code, call
next(gen)to advance normally, then callgen.throw(ValueError, "message")at the point you want to inject the error. The return value of.throw()is the next yielded value after the exception is handled.
▼ Solution & Explanation
Explanation:
try / except ValueErroraroundyield: Wrapping the yield in a try block is what makes the generator resilient to.throw(). Without this, a thrown exception would propagate uncaught, terminating the generator immediately and raising the exception in the caller’s frame instead.gen.throw(ValueError, "simulated error"): RaisesValueError("simulated error")at the exact line where the generator is paused – inside thetryblock. The generator’sexceptclause handles it, prints the message, and the loop continues to the next iteration, yielding3.- Return value of
.throw(): Likenext(),.throw()returns the next value yielded by the generator after the exception is handled. Printing its return value directly captures3without needing a separatenext()call. - Practical use: In async frameworks such as
asyncio,.throw()is used internally to cancel tasks by injecting aCancelledErrorinto a suspended coroutine. Understanding.throw()demystifies how cancellation and timeout logic work beneath theasync/awaitsurface.
Exercise 28: Recursive Directory Walker
Problem Statement: Write a generator that yields all file paths in a directory and its subdirectories using yield from.
Purpose: Recursive directory traversal is one of the most common real-world applications of recursive generators. Using yield from keeps the code concise while avoiding the need to accumulate paths in a list, making it suitable for very large directory trees.
Given Input: A temporary directory tree created at runtime:
test_dir/
file1.txt
file2.txt
subdir/
file3.txt
file4.txt
Expected Output:
test_dir/file1.txt
test_dir/file2.txt
test_dir/subdir/file3.txt
test_dir/subdir/file4.txt
▼ Hint
- Use
os.listdir()to list entries in the current directory. For each entry, build its full path withos.path.join(), then check withos.path.isfile()oros.path.isdir()to decide whether toyieldthe path or recurse into it withyield from. - Create the test directory structure before running the generator using
os.makedirs()andopen(..., "w").close(), so the snippet is self-contained and runnable without any manual setup.
▼ Solution & Explanation
Explanation:
os.path.join(directory, entry): Constructs a platform-correct full path by combining the parent directory with the entry name. Using string concatenation instead would break on Windows, where the path separator is a backslash.yield full_pathvs.yield from walk_files(full_path): Files are yielded directly; directories trigger a recursive call viayield from, which forwards every path produced by the deeper traversal to the outermost caller without any intermediate collection.sorted(os.listdir(...)):os.listdir()returns entries in filesystem order, which is not guaranteed to be alphabetical. Wrapping it insorted()produces consistent, predictable output regardless of the underlying OS or filesystem.- Standard library alternative:
os.walk()andpathlib.Path.rglob("*")both provide recursive directory traversal out of the box. This implementation makes theyield fromrecursion pattern explicit; in production code,pathlib.Path.rglob()is generally preferred for its cleaner API.
Exercise 29: Interleaved Streams
Problem Statement: Write a generator that takes two iterables and yields elements from them alternately.
Purpose: Interleaving streams is a pattern used in merge operations, playlist shuffling, and round-robin task scheduling. This exercise reinforces manual iterator control with iter() and next(), and shows how to handle iterables of unequal length gracefully.
Given Input: a = [1, 2, 3], b = ["a", "b", "c", "d"]
Expected Output: 1 a 2 b 3 c d
▼ Hint
- Convert both inputs to iterators with
iter(). Use awhile Trueloop that alternates between callingnext()on each iterator. Wrap each call in atry/except StopIterationblock, and when one iterator is exhausted,yield fromthe remaining iterator to drain it, thenreturn. - Use two boolean flags or a single counter to track which iterator ran out first, so you know which one still has remaining items to drain after the loop exits.
▼ Solution & Explanation
Explanation:
- Two separate try/except blocks: Each iterator gets its own guard. This structure ensures that exhaustion of either iterator is caught at the precise moment it occurs – after yielding its last item – rather than at the top of the next loop iteration, which would silently drop one item from the other stream.
yield from iter_bthenreturn: Wheniter_ais exhausted, the remaining items initer_bare drained in one step.returnexits the generator cleanly immediately after, preventing the loop from continuing and attempting to read from the already-exhausted iterator again.- Unequal length handling: Because the drain-and-return logic sits inside each except block independently, the generator correctly handles all three cases: both iterables the same length,
iter_ashorter, anditer_bshorter – without any length checks upfront. - Generalising to n streams: This two-stream approach extends naturally by storing iterators in a list and cycling through them with
itertools.cycle, removing exhausted iterators as they raiseStopIteration. That pattern underpins round-robin schedulers and multi-source merge utilities.
Exercise 30: Generator Cleanup
Problem Statement: Use a try...finally block within a generator to ensure a resource (like a database connection) is closed even if the generator is closed early.
Purpose: Generators that hold open resources – file handles, network sockets, database cursors – must release them regardless of whether iteration completes normally or is abandoned. This exercise shows how try...finally inside a generator cooperates with the .close() method to guarantee cleanup, mirroring the behaviour of a context manager.
Given Input: A simulated database with rows [("Alice", 30), ("Bob", 25), ("Carol", 35)]. Close the generator after reading only the first row.
Expected Output:
[DB] Connection opened
Row: ('Alice', 30)
[DB] Connection closed
▼ Hint
- Wrap the entire body of the generator – resource acquisition, the yield loop, and resource release – in a
try...finallyblock. Open (or simulate opening) the resource before the loop, yield rows inside thetry, and close the resource in thefinallyclause. - Call
gen.close()on the generator object after consuming only the first row. Python responds by throwing aGeneratorExitexception into the paused generator at its currentyieldpoint, which causes thefinallyblock to execute before the generator terminates.
▼ Solution & Explanation
Explanation:
database.connect()before thetry: The connection is opened before entering thetryblock because there is nothing to clean up if the connection itself fails. Only resources that have been successfully acquired need to be released infinally.try / finallyaround the yield loop: Thefinallyclause runs unconditionally – whether the loop finishes normally, an exception propagates out, or the generator is closed early via.close(). This is the same guarantee provided by awithstatement and its__exit__method.gen.close(): Throws aGeneratorExitexception into the generator at its currentyieldpoint. The generator must not yield any further values in response – it should only clean up and return. Thefinallyblock executes,disconnect()is called, and the generator terminates.- Relation to context managers: A generator decorated with
@contextlib.contextmanageruses exactly thistry/finallypattern to implement__enter__and__exit__. Everything before the singleyieldruns on entry; thefinallyclause runs on exit, whether thewithblock raised an exception or completed normally.

Leave a Reply