Free Online Code Editor
See how Python generators and the yield keyword let you handle huge files and endless streams one item at a time, with clear examples and common gotchas.
Imagine you have a log file with 50 million lines and you want to find the ones that mention "ERROR". If you load the whole file into a list first, you might run out of memory before you even start.
Generators solve this. They hand you one item at a time, only when you ask for it, and forget it once you've moved on. The memory they use stays small, no matter how much data flows through.
Compare these two ways to produce a million squares:
import sys
as_list = [n * n for n in range(1_000_000)]
as_generator = (n * n for n in range(1_000_000))
print("list size (bytes):", sys.getsizeof(as_list))
print("generator size (bytes):", sys.getsizeof(as_generator))list size (bytes): 8448728
generator size (bytes): 200Your exact numbers may differ a little depending on the Python version, but the gap will be just as dramatic.
The list holds every value at once. The generator holds almost nothing, because it only knows how to compute the next value. The only syntax difference is round brackets instead of square ones.
You can still loop over it or pass it to functions like sum:
print(sum(as_generator))333332833333500000A generator expression is fine for one-liners. For anything bigger, write a generator function. It looks like a normal function, but uses yield instead of return.
def countdown(start):
while start > 0:
yield start
start -= 1
for number in countdown(5):
print(number)5
4
3
2
1When you call countdown(5), nothing runs yet. You get back a generator object. Each time the loop asks for the next value, Python runs the function until it hits yield, hands over that value, and pauses right there. Next time, it picks up exactly where it stopped, with all its local variables intact.
You can see this by calling next() yourself:
gen = countdown(3)
print(next(gen))
print(next(gen))
print(next(gen))3
2
1One more next(gen) would raise StopIteration, which is how Python signals "no more items." A for loop handles that for you quietly.
This is the use case that matters most in real projects. Here's the pattern, using a small in-memory stand-in for a file so you can run it anywhere:
import io
fake_file = io.StringIO(
"INFO start\nERROR disk full\nINFO retry\nERROR timeout\nINFO done\n"
)
def error_lines(lines):
for line in lines:
if "ERROR" in line:
yield line.strip()
for line in error_lines(fake_file):
print(line)ERROR disk full
ERROR timeoutWith a real file, you'd use with open("app.log") as f: for line in error_lines(f):. Python reads one line at a time, so the file can be as large as your disk.
Generators snap together nicely. Each stage takes an iterable and yields results, and nothing is computed until the final loop pulls values through.
def numbers():
n = 1
while True: # endless stream
yield n
n += 1
def only_even(items):
for item in items:
if item % 2 == 0:
yield item
def squared(items):
for item in items:
yield item * item
pipeline = squared(only_even(numbers()))
for _ in range(5):
print(next(pipeline))4
16
36
64
100The numbers() generator never ends, and that's fine. Only the values we actually request ever get computed. You can't do that with a list.
To take the first few items from any iterator, use itertools.islice:
from itertools import islice
print(list(islice(squared(only_even(numbers())), 5)))[4, 16, 36, 64, 100]This catches everyone once. After a generator has been used up, it's empty.
gen = (n for n in range(3))
print(list(gen))
print(list(gen))[0, 1, 2]
[]The second call returns an empty list, with no error. If you need to loop twice, either create the generator again or store the results in a list.
You've been using the iterator protocol this whole time. An iterable is anything you can loop over (lists, strings, files, generators). An iterator is the object doing the work, and it has a __next__ method. Generators are the easiest way to build your own iterator, with no class needed.
Generators aren't always better. Use a list when:
items[3]).Use a generator when the data is large, comes in a stream, or when you may stop early (like "find the first match").
yield pauses a function and remembers where it was.Reading helps, but typing the code is what makes it stick. Open the editor, paste any example from this article, and change it until it breaks.
Open the Python editorA clear order for learning Python from zero, the topics you can safely ignore for now, and the mistakes that trip up almost every beginner.
Learn how Python list comprehensions work, with filtering, nested loops, dict and set versions, and clear rules for when a plain loop is the better choice.
Understand Python decorators by building a timer, a logger, a retry wrapper, and a cache, with the functools.wraps detail most tutorials skip.