We met iter() and next() in the last lesson. This one digs into using them directly — which becomes useful as soon as you want to consume an iterator step by step instead of with a for loop.

The two-argument form of iter()

iter() has a less-known second form: iter(callable, sentinel). It calls the function repeatedly until the function returns the sentinel value.

This is great for reading from a stream until you hit a marker:

# read lines from stdin until an empty one
for line in iter(input, ""):
    print(f"got: {line}")

Each iteration calls input(). When it returns "" (the user just pressed Enter), the loop ends. You can build this kind of pattern without writing a while True: with a break.

next() with a default

next(iterator) raises StopIteration when the iterator is exhausted. To avoid the exception, give it a default:

it = iter([10, 20])
print(next(it))           # 10
print(next(it))           # 20
print(next(it, None))     # None — safe

Without the default, that last call would crash.

Consuming part of an iterator

The point of having next() is being able to walk an iterator one step at a time:

def first_line_of_each(filenames: list[str]) -> list[str]:
    results: list[str] = []
    for name in filenames:
        with open(name) as f:
            results.append(next(f, "").rstrip())
    return results

For each file, we open it, pull one line, and move on. We don’t iterate the rest. This pattern shows up often when processing very large files or streams.

A tiny example — peek at the first item

A common need: look at the first value of an iterator, decide what to do, then process the rest:

data = iter([10, 20, 30, 40])

first = next(data)
print(f"first is {first}")

for value in data:        # the rest
    print(value)
first is 10
20
30
40

The iterator remembers where it left off.

Making your own iterator (advanced)

You can build a custom iterator by writing a class with two methods — __iter__ and __next__. We’ll cover classes properly in Section 12, but here’s a preview:

class Counter:
    def __init__(self, limit: int) -> None:
        self.limit = limit
        self.current = 0

    def __iter__(self) -> "Counter":
        return self

    def __next__(self) -> int:
        if self.current >= self.limit:
            raise StopIteration
        self.current += 1
        return self.current


for n in Counter(5):
    print(n)
1
2
3
4
5

This works — but for almost all real use cases, generators (next lesson) are a far simpler way to build an iterator. You can usually avoid writing this kind of class entirely.

When you’d actually use iter()/next()

You won’t use these every day. But they come up in:

  • Streaming — pulling values from a network, sensor, or file as they arrive
  • Building tokenisers — read one token at a time, decide what to do
  • ML data loadersnext(data_loader) to grab the next batch
  • Lazy pipelines — chain together operations without materialising lists

For typical scripting, for is enough.

What’s next

Next, the cleanest way to build an iterator — generator expressions.

Toggle theme (T)