AI Skill Report Card

Implementing Python Iterator Protocol

A-87·Aug 12, 2026·Source: Web
14 / 15
Python
class Countdown: """An iterable that returns a fresh iterator each time.""" def __init__(self, start): self.start = start def __iter__(self): return CountdownIterator(self.start) class CountdownIterator: """The actual iterator: stateful, implements __next__.""" def __init__(self, current): self.current = current def __iter__(self): return self # iterators must be iterable too def __next__(self): if self.current <= 0: raise StopIteration self.current -= 1 return self.current + 1 for n in Countdown(3): print(n) # 3 2 1

Simpler version using a generator function (preferred in almost all real code):

Python
def countdown(start): while start > 0: yield start start -= 1
Recommendation
Add an example showing correct use of itertools (islice/chain) to reinforce the 'don't reimplement' best practice with actual input/output
13 / 15

Progress:

  • Determine if you need a reusable iterable (supports multiple independent iterations) or a one-shot iterator
  • Choose implementation style: generator function, generator expression, or explicit class
  • Implement __iter__ (and __next__ if a class-based iterator)
  • Ensure StopIteration is raised correctly (or let generator return naturally)
  • Verify with iter(), next(), and a for loop
  • If stateful resource cleanup is needed, consider a context manager instead/alongside

Core distinction

  • Iterable: has __iter__() returning an iterator. Can be iterated over repeatedly, each time producing a new iterator.
  • Iterator: has both __iter__() (returning itself) and __next__() (returning next value or raising StopIteration). Exhausted after one full pass — cannot restart.

Rule of thumb: containers are iterables, not iterators. If __iter__ returns self, the object is single-use.

Choosing an implementation

  1. Generator function (yield) — default choice. Automatically implements the full protocol; state is managed by the interpreter via the frame.
  2. Generator expression — for simple one-liner transformations of an existing iterable: (x*x for x in range(10)).
  3. Explicit class — only when you need:
    • A reusable iterable (separate __iter__ returning fresh iterator state each call)
    • Extra methods/attributes on the iterator itself
    • Fine control not expressible with yield (rare)
Recommendation
Include a brief example demonstrating the two-arg iter(callable, sentinel) pattern in the Examples section, not just mentioned in passing
18 / 20

Example 1: Iterable vs iterator bug

Input:

Python
class Numbers: def __init__(self, data): self.data = data self.i = 0 def __iter__(self): return self def __next__(self): if self.i >= len(self.data): raise StopIteration v = self.data[self.i] self.i += 1 return v nums = Numbers([1, 2, 3]) print(list(nums)) print(list(nums)) # second call — what happens?

Output:

[1, 2, 3]
[]

Explanation: Numbers is its own iterator (__iter__ returns self), so it's exhausted after the first pass. Fix by separating iterable state (data) from iterator state (i) into two classes, or by making __iter__ return a fresh generator: def __iter__(self): return iter(self.data).

Example 2: Infinite iterator with manual protocol

Input:

Python
def take(iterable, n): it = iter(iterable) result = [] for _ in range(n): result.append(next(it)) return result def naturals(): n = 1 while True: yield n n += 1 take(naturals(), 5)

Output: [1, 2, 3, 4, 5]

Example 3: Using next() with a default to avoid StopIteration

Input:

Python
it = iter([]) value = next(it, "empty")

Output: "empty" (no exception raised)

Recommendation
Consider a small section on async iteration (__aiter__/__anext__) as a related edge case, or explicitly note it's out of scope
  • Prefer generator functions over hand-written __next__ classes — less state to manage manually.
  • If a class represents a collection, give it __iter__ that returns a new generator/iterator each call, so it can be iterated multiple times (e.g., in nested loops, zip, list comprehensions used twice).
  • Use iter(obj, sentinel) two-arg form to turn callables into iterators that stop at a sentinel value (e.g., reading until EOF): iter(file.readline, '').
  • Combine with itertools (islice, chain, takewhile) instead of reimplementing common iterator patterns.
  • Let StopIteration propagate naturally from a generator by simply returning — don't raise it manually inside a generator body (deprecated behavior, was silently swallowed pre-3.7, now RuntimeError).
  • Document whether a returned object is a one-shot iterator or a re-iterable, since callers can't tell just from a for loop working once.
  • Exhaustion confusion: reusing an iterator (not iterable) across multiple loops silently yields nothing the second time — no error, just empty results.
  • Raising StopIteration manually inside a generator: causes RuntimeError: generator raised StopIteration (PEP 479). Use return instead.
  • Forgetting __iter__ returns self in a class-based iterator — without it, for loops fail with TypeError: iter() returned non-iterator.
  • Assuming len() works on iterators — iterators don't know their remaining length in general; don't rely on len(it).
  • Mutating a collection while iterating over it directly — causes skipped elements or RuntimeError (for dicts/sets). Iterate over a copy (list(d.items())) if mutation is needed.
  • Mixing up iterable and iterator in type hints/docs — e.g., typing a parameter as Iterator[int] when callers should be able to pass a list (which is Iterable[int], not Iterator[int]) forces unnecessary iter() calls on callers.
0
Grade A-AI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
14/15
Workflow
13/15
Examples
18/20
Completeness
18/20
Format
15/15
Conciseness
14/15