Python for AIPython 3.12 · Pydantic 2.12
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
43 small wins to finish your pathNext lesson →

Iterators and generators

An iterator is an object that hands you its values one at a time, and a generator is a function that writes one for you by using yield in place of return.

Last updated: 13 Oct, 2026 · Python 3.12

The Inheritance lesson built objects that share behaviour. This lesson is about objects that produce a sequence of values on demand, one value per request, without ever holding the whole sequence at once.

A list builds every value up front. An iterator does not. It keeps its place and produces the next value only when you ask for it. That is what lets a program read a file larger than memory, or print a model's reply one token at a time as each piece arrives.

A list [10, 20, 30] is an iterable; iter() turns it into an iterator that has computed nothing yet. Each call to next() pulls one value at a time: the first next() returns 10, the second 20, the third 30, and a fourth next() raises StopIteration because the iterator is exhausted.
Iterables versus iterators · from Tutorial 19- Python Iterables vs Iterators · 2:08 to 6:14
From the video. The clip takes a list such as [1, 2, 3, 4, 5, 6], turns it into an iterator with iter(), and pulls values with next() in a Jupyter notebook, where a cell's last value shows without print(). In a .py file, wrap the value in print(), as the runs below do.

Syntax: iter() turns an iterable into an iterator; next() pulls the next value and raises StopIteration when there are none left.

python
it = iter([1, 2, 3, 4, 5, 6])   # an iterator over the list
first = next(it)                # 1, the first value
# calling next again gives 2, then 3, and so on, then StopIteration

Pulling values with next

A list is an iterable: something you can loop over. iter() turns it into an iterator, which remembers how far you have read. Each next() returns the next value. A string is iterable too, so the same iter/next pair walks it one character at a time.

ExamplePulling values out of a list iterator, then a string iterator
my_list = [1, 2, 3, 4, 5, 6]
it = iter(my_list)        # a reader positioned at the start
print(type(it).__name__)
print(next(it))
print(next(it))
print(next(it))

# a string is iterable too: its iterator hands back one character at a time
my_string = "Hello"
chars = iter(my_string)
print(next(chars))
print(next(chars))

What the next calls produced

  • list_iterator is the kind of object iter() returned. It is not the list; it is a reader positioned at the start.
  • 1, 2, 3 came out one per next() call, in order. The iterator moved its position forward each time, with 4, 5 and 6 still waiting.
  • H, e came from the string iterator the same way, proving iter and next work on any iterable, not only lists.

When an iterator has nothing left, the next next() raises StopIteration. Catching it is how you know an iterator is spent; a for loop does this for you and stops.

ExampleThe third next() on a two-item list raises StopIteration
it = iter([1, 2])

try:
    print(next(it))
    print(next(it))
    print(next(it))      # the list is exhausted, so this raises
except StopIteration:
    print("There are no elements in the iterator")

The first two next() calls returned 1 and 2. The third found nothing left and raised StopIteration, which the except caught. This is exactly what a for loop relies on to know when to stop.

Writing a generator with yield

Writing an iterator class by hand is work. A generator is the short way: an ordinary function that uses yield. Calling it does not run the body. It returns a generator object, and each value comes out at a yield when you ask for the next one. my_generator below yields three values in turn.

python
def my_generator():
    yield 1        # hand back a value, then pause here until the next request
    yield 2
    yield 3
ExampleA generator handing back one value per yield
def my_generator():
    yield 1
    yield 2
    yield 3

gen = my_generator()          # nothing has run yet; this is a generator object
print(type(gen).__name__)
for value in gen:
    print(value)

Calling my_generator() built a generator object and ran none of the body. The for loop drove it: each pass asked for the next value, the function ran up to the next yield, handed back a number, and paused there until the next pass. After yield 3 the function ended, which is the generator's StopIteration, and the loop stopped.

Reading a large file lazily

The payoff is that a generator never has to hold everything at once. His read_large_file yields one line at a time, so a file of any size costs the same memory as a single line. The example writes a small file to a temporary folder first, then reads it and stops early to show that lines past the stop were never produced.

python
def read_large_file(file_path):
    with open(file_path) as file:
        for line in file:       # the file yields one line at a time
            yield line.rstrip("\n")
ExampleReading only as far as it needs, never the whole file
import os, tempfile

# write a small five-line file to a temp folder so there is something to read back
path = os.path.join(tempfile.mkdtemp(), "log.txt")
with open(path, "w") as f:
    for i in range(1, 6):
        f.write(f"line {i}\n")

def read_large_file(file_path):
    with open(file_path) as file:
        for line in file:
            yield line.rstrip("\n")

# stop at line 3 to prove lines 4 and 5 are never produced
for line in read_large_file(path):
    print(line)
    if line == "line 3":
        break

The loop printed three lines and stopped at line 3. Lines 4 and 5 sat in the file and were never read, because the generator produces a line only when the loop asks. The same one-at-a-time delivery is how a chat interface prints a model's reply token by token: each token is yielded and shown as it arrives, instead of waiting for the whole answer.

List vs generator

listgenerator
Valuesall built up frontone at a time, on demand
Memoryholds every valueholds only its current place
Second passloop over it againexhausted after one pass
Lengthlen() worksno len(), you consume it
Good forsmall collections you reuselarge or streamed data

Where iterators and generators show up in AI code

  • Reading a dataset too large to fit in memory, one record or one line at a time.
  • Streaming a model's reply, printing each token as it arrives instead of waiting for the full answer.
  • Building a pipeline where each stage yields to the next, with no large intermediate list between them.
Watch out. A generator is used up after one pass. Looping over the same generator a second time yields nothing, with no error, so a count or a sum on the second loop comes back empty. If you need the values again, build a list from them once with list(gen) and loop over that.
Try it yourself
  • Change the stop line in the file reader to if line == "line 5" and watch all five lines print.
  • After the for loop over my_generator(), loop a second time over the same gen. Nothing prints, because it is exhausted.
  • Turn the file reader into a token streamer: split each line on spaces and yield one word at a time.
PreviousInheritance

This is what real progress feels like.