datarekha
Python Medium Asked at GoogleAsked at AmazonAsked at MetaAsked at Databricks

What is the difference between a generator and a list, and when should you prefer a generator?

The short answer

A list materialises all values in memory at once; a generator produces values one at a time on demand, using O(1) memory regardless of the sequence length. Prefer generators for large or infinite sequences, pipelines, and any situation where you do not need random access.

How to think about it

A list is a finished container — every value computed and sitting in memory. A generator is a recipe — it computes the next value only when something asks for it. That single difference in when the work happens is the whole question, and the memory gap it opens is dramatic: a list of ten million squares costs tens of megabytes, while the equivalent generator costs about a hundred bytes, because at any instant it is holding one number, not ten million.

The price for that thrift is that a generator is single-pass. Walk it once and it’s spent — no second loop, no indexing. If you need the values more than once, you either keep a list or rebuild the generator.

The syntax is one character apart

A list comprehension builds everything now; swap the [] for () and you get a lazy generator expression instead:

import sys

big_list = [x * x for x in range(10_000_000)]
print(sys.getsizeof(big_list))   # tens of MB — all ten million stored

big_gen = (x * x for x in range(10_000_000))
print(sys.getsizeof(big_gen))    # ~100 bytes — nothing computed yet

A generator function uses yield to suspend and resume, which is what lets you stream a file far larger than memory:

def read_chunks(filepath, size=4096):
    with open(filepath, "rb") as f:
        while chunk := f.read(size):
            yield chunk

for chunk in read_chunks("dataset.bin"):
    process(chunk)   # only one chunk in memory at a time
list [0..N]All N items in RAMgeneratorOne item per next() call
Lists allocate all items upfront; generators yield one item per next() call.

A worked example

Three things at once: the memory gap on a small list, lazy production value-by-value, and the single-pass trap that bites everyone:

import sys

# Memory: same 1000 values, wildly different footprints
small_list = [x * x for x in range(1000)]
small_gen  = (x * x for x in range(1000))
print(f"list size : {sys.getsizeof(small_list):,} bytes")
print(f"gen size  : {sys.getsizeof(small_gen):,} bytes")

# Lazy: a generator hands over one value per next()
def count_up(limit):
    n = 0
    while n < limit:
        yield n
        n += 1

gen = count_up(5)
print()
print("Pulling values one at a time:")
print(" next():", next(gen))
print(" next():", next(gen))
print(" rest  :", list(gen))         # drain whatever's left

# Single-pass: a drained generator is empty forever after
gen2 = (x for x in range(5))
print()
print("First pass :", list(gen2))
print("Second pass:", list(gen2))     # already exhausted

# Pipeline: each stage is a generator, the whole chain is constant-memory
def evens(n):     return (x for x in range(n) if x % 2 == 0)
def squares(seq): return (x * x for x in seq)

print()
print("Pipeline result:", list(squares(evens(10))))
list size : 8,856 bytes
gen size  : 112 bytes

Pulling values one at a time:
 next(): 0
 next(): 1
 rest  : [2, 3, 4]

First pass : [0, 1, 2, 3, 4]
Second pass: []

Pipeline result: [0, 4, 16, 36, 64]

The numbers tell the story: a thousand stored integers take 8,856 bytes, the generator that can produce the same thousand takes 112, because it stores a recipe, not results. And look at the second pass — []. The generator wasn’t reset; it was consumed.

They compose into pipelines

The real payoff is composability. Stack generators and the whole chain runs in constant memory, with the final consumer — here sum — pulling every stage one value at a time:

lines   = (line.strip() for line in open("log.txt"))
records = (line.split(",") for line in lines if line)
values  = (float(r[2]) for r in records)
total   = sum(values)   # the entire pipeline runs in constant memory

The idea underneath

Every for loop, every sum, list, and join, drives a sequence by calling next() over and over. A generator simply chooses to do one value’s worth of work per call instead of all of it upfront. That’s the entire model; yield is just Python’s way of letting a function pause and pick up where it left off.

Learn it properly Generators

Keep practising

All Python questions

Explore further

Skip to content