Python, section 7 of 9, 3h

Generators and working with large data

By the end: You can process files larger than memory and consume a streaming response.

  • What an iterator is
  • Generators and yield
  • Why they save memory
  • Batching
  • Async generators for streaming

New to python? A programming language is the set of written instructions a computer will follow, and Python is the one this field settled on. It reads close to English: a line that filters a list of records looks roughly like a sentence describing that filter. Every model provider ships a Python library, and nearly every machine learning tool is written in it. Why it matters

A list holds everything at once, which is fine until the everything is a ten-gigabyte file. A generator hands you one item at a time and remembers where it got to, so you can work through data far larger than memory, and start handling a model's reply before it has finished arriving. This section covers what an iterator is, how to write a generator, how to batch, and the async version used for streaming responses.

The idea#

A normal function computes everything and returns it all at once. A generator produces one value at a time and pauses in between, remembering where it was. Nothing is computed until you ask for it.

# Returns a list of 1,000,000 numbers. All in memory at once.
def squares_list(n):
    return [i * i for i in range(n)]

# Produces them one at a time. Almost no memory.
def squares_gen(n):
    for i in range(n):
        yield i * i

for value in squares_gen(1_000_000):
    print(value)
yield instead of return. That one word is the whole difference.

Why this matters#

Reading a very large file with read() loads all of it into memory and may fail. Looping over the file object reads one line at a time, so the size of the file stops mattering.

# Loads the whole file
with open("huge.jsonl") as f:
    lines = f.readlines()

# Reads one line at a time
def records(path):
    with open(path) as f:
        for line in f:
            yield json.loads(line)

for record in records("huge.jsonl"):
    process(record)

Generator expressions#

A comprehension with round brackets instead of square ones is a generator. Use it when you are going to loop over the result once and do not need to keep it.

squares = [n * n for n in range(1000)]     # a list, held in memory
squares = (n * n for n in range(1000))     # a generator, produced on demand

total = sum(n * n for n in range(1000))    # no list is ever built

Batching#

Most embedding and inference APIs take batches, so you will regularly need to turn a long stream into fixed-size groups.

from itertools import batched

for chunk in batched(records("huge.jsonl"), 64):
    embed(chunk)        # a tuple of up to 64 items

# Before Python 3.12, written by hand
def batched_manual(items, size):
    batch = []
    for item in items:
        batch.append(item)
        if len(batch) == size:
            yield batch
            batch = []
    if batch:
        yield batch      # do not lose the last partial batch

A generator can only be used once

After you loop over it, it is empty. Looping again gives you nothing and raises no error. In a retrieval pipeline this looks exactly like your documents disappearing. If you need the data twice, convert it to a list and accept the memory cost knowingly.

Streaming responses#

When an API streams a reply, it arrives as an async generator. You loop with async for, and each pass waits for the next piece.

async def collect(stream):
    parts = []
    async for chunk in stream:
        parts.append(chunk.text)
        print(chunk.text, end="", flush=True)   # show it as it arrives
    return "".join(parts)

One consequence worth remembering: you cannot check the shape of a streamed response until it has finished. Any JSON parsing or schema validation has to happen after the loop, not inside it.

Where people go wrong

Building a list when a generator would do. It works fine on your test file and runs out of memory on the real one, usually in production and usually at the worst moment.

If this comes up in an interview

What is the difference between a list and a generator?

A list computes and stores every item immediately. A generator produces items one at a time on demand and stores only its current position, so memory stays flat no matter how many items there are.

How would you process a file larger than your available RAM?

Loop over the file object rather than calling read or readlines, so it yields one line at a time. Wrap it in a generator if you need to transform the records, and batch it if the next step wants groups.

What happens when you iterate a generator a second time?

You get nothing, and no error. It has been consumed. If you need the values again you must recreate the generator or materialise it into a list.

Can you call len() on a generator?

No. A generator does not know how many items it will produce and cannot look ahead. If you need a count, either convert it to a list or count as you iterate.

Build this: Write a generator that yields fixed-size batches from any iterable without loading everything, then use it to process a file much larger than your RAM and report how many batches it produced.

This is one section of Python, 33 hours across 9 sections. Read it in full to get the whole topic in order.

Stop 1 of 50 on the AI Engineer route

0 of 50 topics done · 0h behind you, 603h to go

Next on this routeHTTP & APIsRequests, status codes, retries, rate limits.8h

What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.