Two classes that both work in a for loop. Only one of them works in two of them.
# An iterator: it is its own cursor, and it is used up
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current < 1:
raise StopIteration
self.current -= 1
return self.current + 1
c = Countdown(3)
list(c) # -> [3, 2, 1]
list(c) # -> [] the state is gone
# An iterable: it hands out a fresh cursor each time
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self):
return iter(range(self.start, 0, -1))
- for calls iter() once, then next() until StopIteration
That is the entire protocol. `for n in thing` is iter(thing) followed by repeated next() calls, and the loop ends when StopIteration is raised. Nothing else is involved — no length, no indexing — which is why anything with a sensible __iter__ works in a for loop, a comprehension, sum(), min(), and the `in` operator.
- An iterable makes iterators; an iterator is one
A list is an iterable: iter(list) gives you a brand new cursor every time, which is why you can loop over it repeatedly. A generator is an iterator: iter(generator) returns the generator itself, so a second loop finds it already at the end. Returning `self` from __iter__ is what makes a class the second kind — usually by accident.
- A generator method gives you a fresh iterator for free
If __iter__ contains a yield, calling it returns a new generator object each time — so the object stays an iterable and every loop gets its own state, with no cursor to store on self. It is the shortest correct answer and it makes the two-loops-at-once case work without you thinking about it.
Store the starting number and nothing else — no current position. All the work happens in __iter__, which is called afresh at the start of every loop.
Make __iter__ a generator by putting a yield in it: each call then returns a new generator with its own state, so two loops cannot interfere. Validate the start in __init__, not in __iter__ — otherwise the error only appears when somebody loops.
range(self.start, 0, -1) counts down and stops before zero. Either yield from it or return iter() of it — both give a fresh iterator per call. __len__ is just self.start, once negatives are refused.
class Countdown: def __init__(self, start): if start < 0: raise ____(f’start cannot be negative, got {start}') self.start = start def __iter__(self): ____ from range(self.start, ____, -1) def __len__(self): return self.____
class Countdown:
"""Counts down from start to 1, as often as you like."""
def __init__(self, start):
if start < 0:
raise ValueError(f"start cannot be negative, got {start}")
self.start = start
def __iter__(self):
# A yield makes this a generator function, so every call returns a
# brand new iterator with its own position.
yield from range(self.start, 0, -1)
def __len__(self):
return self.start
There is nowhere for two loops to collide, because the object stores only what it is — a starting number — and never where anybody has got to. Any state describing progress belongs to the iterator, not the iterable. When a class has both a self.index and an __iter__ returning self, that is the bug in advance.
Both work here and they differ in what the method *is*: with a yield, __iter__ is a generator function, so the body does not run until somebody iterates and you can put logic between the yields later. Returning iter(range(...)) hands off to range’s own iterator, which is marginally faster and closes the door on doing anything per item.
A for loop never calls __len__ — plenty of perfectly good iterables have no length at all, including every generator and anything infinite. Adding it says something extra: that the size is known in advance and cheap to get. It also makes bool(Countdown(0)) False, since Python falls back to the length when there is no __bool__, which may or may not be what you meant.
If a plain range() or a generator function would do, use one — a class earns its place when the thing has an identity beyond the sequence it produces: a Deck you also shuffle, a Page you also paginate, a Query you also filter. And if you genuinely want single-pass semantics, say so loudly: return self from __iter__ and document that it is an iterator, because a caller who assumes otherwise gets an empty second loop and no error. collections.abc.Iterable and Sequence are worth inheriting from when the class should pass isinstance checks and get the mixin methods.
- Add a step so Countdown(10, step=2) gives 10, 8, 6, 4, 2, and decide what len() is then.
- Implement __contains__ so membership is arithmetic rather than a loop, and check it against the iteration for a few values.
- Deliberately write the iterator version — __iter__ returning self — and confirm exactly which of the cases above it fails.