devnotes.

Iterators and Iterables

Understanding the iterator protocol explains why for loops work uniformly across lists, strings, files, and custom objects — it's a simple, two-method contract underneath all of it.

numbers = [1, 2, 3]
it = iter(numbers)          # get an iterator from an iterable
print(next(it))               # 1
print(next(it))               # 2
print(next(it))               # 3
# next(it)                   # ❌ raises StopIteration — exhausted

class CountUp:                 # a custom iterable/iterator
    def __init__(self, limit):
        self.limit = limit
        self.current = 0

    def __iter__(self):          # makes CountUp itself iterable
        return self

    def __next__(self):           # defines what happens on each iteration
        if self.current >= self.limit:
            raise StopIteration    # signals "no more items"
        self.current += 1
        return self.current

for n in CountUp(5):     # works exactly like a built-in iterable in a for loop
    print(n)               # 1, 2, 3, 4, 5

An iterable is any object with an __iter__ method (returning an iterator); an iterator is any object with both __iter__ (returning itself) and __next__ (returning the next value, or raising StopIteration when exhausted). A for loop is essentially syntax sugar over calling iter() once and then next() repeatedly until StopIteration is raised — this is exactly why any class implementing __iter__/__next__ works seamlessly in a for loop, list comprehension, or with sum()/list()/any other function expecting an iterable.

Once an iterator is exhausted, it stays exhausted — calling next() again still raises StopIteration, and re-iterating requires calling iter() again on the original iterable to get a fresh iterator (this is why iterating over a generator, covered next, twice in a row silently produces nothing the second time).

Iterators and Iterables in Python | DevNotes