Python & Django

Generators: Lazy Evaluation in Python

August 17, 2026
3 min read
1 views

<p>Generators let you produce a sequence of values over time, without building the whole sequence in memory at once. They're one of Python's most elegant features once they click.</p>

<h2>The Basic Idea</h2>
<p>A generator function looks like a normal function but uses <code>yield</code> instead of <code>return</code>:</p>

<pre><code>def count_up_to(n):
    i = 1
    while i <= n:
        yield i
        i += 1

for num in count_up_to(5):
    print(num)
# 1 2 3 4 5
</code></pre>
<p>Calling <code>count_up_to(5)</code> doesn't run the function body immediately — it returns a generator object. Code only executes as values are requested, one <code>yield</code> at a time.</p>

<h2>Why Not Just Use a List?</h2>
<p>Consider processing a huge file:</p>

<pre><code>def read_large_file(path):
    with open(path) as f:
        for line in f:
            yield line.strip()

for line in read_large_file("huge_log.txt"):
    process(line)
</code></pre>
<p>This never loads the whole file into memory — only one line exists at a time. Compare that to <code>f.readlines()</code>, which builds a full list upfront. For a 10GB log file, that difference is the difference between running fine and crashing with a memory error.</p>

<h2>Generator Expressions</h2>
<p>Just like list comprehensions, but lazy:</p>

<pre><code>squares = (x * x for x in range(1_000_000))  # generator, no memory used yet
total = sum(squares)                          # values produced one at a time
</code></pre>
<p>Swap the brackets <code>[]</code> for parentheses <code>()</code> and a list comprehension becomes a generator expression.</p>

<h2>State Is Preserved Between Calls</h2>
<p>A generator remembers exactly where it left off, including local variables:</p>

<pre><code>def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
print(next(fib))  # 0
print(next(fib))  # 1
print(next(fib))  # 1
print(next(fib))  # 2
</code></pre>
<p>This makes generators perfect for infinite or unbounded sequences — something a list could never represent.</p>

<h2>yield from: Delegating to Sub-Generators</h2>
<pre><code>def inner():
    yield 1
    yield 2

def outer():
    yield 0
    yield from inner()
    yield 3

print(list(outer()))  # [0, 1, 2, 3]
</code></pre>

<h2>Sending Values Into a Generator</h2>
<p>Generators can also receive values, turning them into simple coroutines:</p>

<pre><code>def running_total():
    total = 0
    while True:
        value = yield total
        total += value

gen = running_total()
next(gen)          # prime the generator
print(gen.send(5))  # 5
print(gen.send(3))  # 8
</code></pre>

<h2>Key Takeaway</h2>
<p>Generators trade eagerness for laziness: instead of computing everything up front, they compute one value at a time, exactly when needed. This makes them the natural choice for large datasets, infinite sequences, and streaming pipelines where memory efficiency matters.</p>

Topics

Python
MAR

MD Abdur Rahim

Senior Python Developer helping teams ship backend systems and AI products — Django, FastAPI, LangChain, RAG pipelines, and cloud infra that hold up in production.

Comments (0)

Minimum 3 characters

0/1000

No comments yet

Be the first to share your thoughts!

Enjoyed this article?

Subscribe to my newsletter to receive updates on new blog posts, tech insights, and development tips.

No spam. Unsubscribe anytime. Read our Privacy Policy.