When a for loop needs another value, it calls next() on an iterator, which invokes __next__. The method returns one value at a time and raises StopIteration when the sequence is exhausted. section 10.0
A custom iterator stores its position on the object because each __next__ call starts with a fresh local scope. It checks the position, returns the current item, advances the position, and eventually raises StopIteration. section 10.2
A stateful iterator
class LoudIterator: def __init__(self, data): self.data = data self.index = 0 def __iter__(self): return self def __next__(self): if self.index >= len(self.data): raise StopIteration value = self.data[self.index] self.index += 1 return value for item in LoudIterator('abc'): print(item) The loop prints a, b, and c, then stops when __next__ raises StopIteration. section 10.0
Returning self from __iter__ is convenient for a single-use iterator, but reusing the same object leaves its index exhausted. A separate iterator class lets the iterable produce a fresh state for each loop. section 10.2
A class does not have to implement iteration with __next__ directly: __iter__ may return a helper iterator or a generator. Generator functions preserve their position between calls to __next__. section 10.7 section 10.22

