An iterable is anything you can loop over (list, dict, str, set…). It implements __iter__().
An iterator is the object that actually produces values one at a time, tracking position. It implements both __iter__() and __next__().
nums = [1, 2, 3] # nums is an ITERABLEit = iter(nums) # calling iter() on it produces an ITERATORnext(it) # 1next(it) # 2next(it) # 3next(it) # StopIteration! no more items
What a for loop does internally
for x in nums: print(x)
is roughly equivalent to:
it = iter(nums)while True: try: x = next(it) except StopIteration: break print(x)
Building a Custom Iterator
Implement __iter__ (returns self) and __next__ (returns next value, raises StopIteration when done).
class CountUp: def __init__(self, start, end): self.current = start self.end = end def __iter__(self): return self def __next__(self): if self.current > self.end: raise StopIteration value = self.current self.current += 1 return valuefor n in CountUp(1, 5): print(n) # 1 2 3 4 5
Separating Iterable from Iterator (The Correct Pattern for Reusability)
The above CountUp example has a flaw: once exhausted, it cannot be iterated again (the state is stuck at end + 1). The standard fix is to separate the container (iterable) from the iterator it produces.
class NumberRange: # ITERABLE: knows the data, produces fresh iterators def __init__(self, start, end): self.start = start self.end = end def __iter__(self): return NumberRangeIterator(self.start, self.end)class NumberRangeIterator: # ITERATOR: tracks position for ONE pass def __init__(self, current, end): self.current = current self.end = end def __iter__(self): return self def __next__(self): if self.current > self.end: raise StopIteration value = self.current self.current += 1 return valuer = NumberRange(1, 3)list(r) # [1, 2, 3]list(r) # [1, 2, 3] again! each `iter(r)` call creates a fresh iterator
Manually Draining an Iterator
it = iter([1, 2, 3])next(it, "default") # 1, second argument avoids StopIteration if exhaustednext(it, "default") # 2next(it, "default") # 3next(it, "default") # 'default', instead of raising
Built-in Functions That Work on Any Iterable
list(range(5))sum(x for x in range(5))max([3, 1, 4])sorted("dcba")any(x > 2 for x in [1, 2, 3])all(x > 0 for x in [1, 2, 3])
See Generators for the far more common, easier way to build custom iterators using yield.