Intermediate20 min

Comprehensions and generators

Transform collections in one readable line, and stream large datasets without loading them into memory.

Why this matters in AI / ML / GenAI

Comprehensions build token lists, filter chunks, and reshape records constantly. Generators matter more: training data and log files are too big for RAM, so you yield batches instead of building a giant list.

List, dict, and set comprehensions

[expr for item in seq] builds a list. Add a filter: [x for x in scores if x > 0.7].

Dict version: {k: v for k, v in pairs}. Set version uses braces with a single expression.

Rule of thumb: one loop and one condition is fine; anything more nested belongs in a normal for-loop. Readability wins — a reviewer should understand it at a glance.

Generators: lazy sequences

Replace the brackets with parentheses and you get a generator expression — values are produced on demand, not stored.

A generator function uses yield instead of return. Each yield hands one value to the caller and pauses; the next iteration resumes right there.

Why it matters: streaming a 50 GB JSONL of training records with for record in read_records(path) uses constant memory. The same pattern powers token streaming from an LLM, where each chunk arrives one at a time.

Generators are single-use. Once consumed, iterate again by calling the function again.

Copy-paste examples

Copy into your own editor, or load one into the compiler below and press Run.

Clean and filter in one line

Read it as: keep the stripped lowercase text for each doc that is long enough.

docs = ["  Python  ", "AI", "  Machine Learning ", "ML"]
cleaned = [d.strip().lower() for d in docs if len(d.strip()) > 2]
print(cleaned)

lengths = {d.strip(): len(d.strip()) for d in docs}
print(lengths)

Generator that yields batches

This is the shape of a data loader. Memory stays flat regardless of dataset size.

def batched(items, size):
    batch = []
    for item in items:
        batch.append(item)
        if len(batch) == size:
            yield batch
            batch = []
    if batch:
        yield batch

rows = list(range(10))
for i, batch in enumerate(batched(rows, 4), start=1):
    print("batch", i, batch)

Streaming tokens (simulated)

Real LLM streaming has the same interface: iterate and handle each piece.

def stream_tokens(text):
    for word in text.split():
        yield word

collected = ""
for token in stream_tokens("Retrieval augmented generation grounds answers"):
    collected += token + " "
    print("received:", token)
print("final:", collected.strip())

Filter retrieved chunks by score

Try it — in-browser Python

Lower the threshold to 0.5 and compare how many chunks survive.

Output

Python runs in your browser. First run downloads the runtime.

Press Run (or Ctrl+Enter) to execute.

CPython in WebAssembly. Stdlib works. NumPy and pandas load on demand. No input(), no GPU, no network installs.

Takeaways

  • Comprehensions replace short build-a-list loops; keep them to one loop and one filter.
  • Generators use yield and stream data with constant memory.
  • Generators are single-use — call the function again to re-iterate.