AI Skill Report Card
Implementing Python Iterator Protocol
Quick Start14 / 15
Pythonclass 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):
Pythondef 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
Workflow13 / 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
StopIterationis raised correctly (or let generator return naturally) - Verify with
iter(),next(), and aforloop - 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 raisingStopIteration). 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
- Generator function (
yield) — default choice. Automatically implements the full protocol; state is managed by the interpreter via the frame. - Generator expression — for simple one-liner transformations of an existing iterable:
(x*x for x in range(10)). - 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)
- A reusable iterable (separate
Recommendation▾
Include a brief example demonstrating the two-arg iter(callable, sentinel) pattern in the Examples section, not just mentioned in passing
Examples18 / 20
Example 1: Iterable vs iterator bug
Input:
Pythonclass 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:
Pythondef 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:
Pythonit = 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
Best Practices
- 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
StopIterationpropagate naturally from a generator by simplyreturning — don't raise it manually inside a generator body (deprecated behavior, was silently swallowed pre-3.7, nowRuntimeError). - Document whether a returned object is a one-shot iterator or a re-iterable, since callers can't tell just from a
forloop working once.
Common Pitfalls
- Exhaustion confusion: reusing an iterator (not iterable) across multiple loops silently yields nothing the second time — no error, just empty results.
- Raising
StopIterationmanually inside a generator: causesRuntimeError: generator raised StopIteration(PEP 479). Usereturninstead. - Forgetting
__iter__returnsselfin a class-based iterator — without it,forloops fail withTypeError: iter() returned non-iterator. - Assuming
len()works on iterators — iterators don't know their remaining length in general; don't rely onlen(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
iterableanditeratorin type hints/docs — e.g., typing a parameter asIterator[int]when callers should be able to pass a list (which isIterable[int], notIterator[int]) forces unnecessaryiter()calls on callers.