Senior 5 min · March 05, 2026

Python Generators — The Empty Log Report Bug

A log pipeline that ran fine for weeks suddenly outputs zero results — that's generator exhaustion.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Generator functions use yield to pause and resume execution, freezing local state
  • Calling a generator function returns an object — the body runs only when next() or for loop starts
  • Memory stays O(1): only one value exists at a time, regardless of dataset size
  • Performance cost: ~40ns overhead per yield call vs direct iteration; negligible for I/O-bound pipelines
  • Production trap: exhaust a generator once and it's gone forever — silent empty iterations follow
  • Biggest mistake: assuming the function runs at call time; side effects never fire until iteration
Plain-English First

Imagine a vending machine that makes each snack on demand the moment you press a button — instead of baking every snack upfront and stuffing them all into a huge bag you have to carry. A Python generator is that vending machine. It produces values one at a time, only when you ask for the next one, and it remembers exactly where it left off each time. You get the same snacks, but without the heavy bag.

Every Python developer hits a wall: they write a reasonable script that loads a dataset, processes it, and crashes — not because the logic is wrong, but because they tried to hold a million rows in memory all at once. It's one of the most common avoidable performance problems, and generators exist to solve it. They're not niche; they power Python's own range(), map(), and zip().

The core problem is the cost of 'eagerness'. A regular list computes and stores every value immediately. Fine for 100 items. A disaster for 10 million log entries, infinite sequences, or streaming API responses where you don't even know the final count. Generators flip the model: they're lazy, producing each value only when the caller asks for the next one. Memory stays flat no matter how large the dataset.

By the end you'll understand why yield exists and how it differs from return, you'll write generator functions and generator expressions with confidence, and you'll know the real-world patterns — log file processing, data pipelines, infinite sequences — where generators genuinely shine. You'll also avoid the two traps that catch almost every developer the first time.

What yield Actually Does — and Why It's Not Just a Fancy return

The single most important thing to understand about generators is what happens to the function's execution state when it hits yield. With a normal return, the function runs, hands back a value, and is completely torn down — local variables gone, position in code gone, everything erased. When a function hits yield, Python does something different: it pauses the function, hands the yielded value to the caller, and freezes the entire execution frame in place — local variables, loop counters, everything. The next time the caller asks for a value by calling next(), Python thaws that frozen frame and continues from the exact line after yield.

This is why a generator function doesn't execute at all when you call it. Calling a generator function just returns a generator object. The body doesn't run until you start consuming that object with next() or a for loop. That single distinction trips up almost every developer the first time.

io/thecodeforge/generators/basic_demo.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import sys

# io.thecodeforge — Basic Generator Implementation
def count_up_to(maximum):
    current = 1
    while current <= maximum:
        # State is frozen right here
        yield current
        # Resumes here on the next call
        current += 1

# 1. Calling the function returns the object, does NOT execute the body
counter = count_up_to(5)
print(f"Object type: {type(counter)}")

# 2. Manual consumption
print(f"First value: {next(counter)}")

# 3. Iteration (handles StopIteration automatically)
for number in counter:
    print(f"Iterated: {number}")

# 4. Memory comparison
eager_list = [i for i in range(10000)]
lazy_gen = (i for i in range(10000))

print(f"List Size: {sys.getsizeof(eager_list)} bytes")
print(f"Gen Size:  {sys.getsizeof(lazy_gen)} bytes")
Output
Object type: <class 'generator'>
First value: 1
Iterated: 2
Iterated: 3
Iterated: 4
Iterated: 5
List Size: 85176 bytes
Gen Size: 112 bytes
Watch Out:
Calling a generator function returns a generator object instantly — zero code in the function body runs at that point. If you forget this and expect side effects to happen on call, you'll get silent bugs. The body only runs when you start iterating.
Production Insight
In production, we've seen whole monitoring pipelines stay dark because the generator was never iterated — the developer expected side effects on function call.
If you need both setup and lazy iteration, use a class with __iter__ and __next__, not a generator function.
Key Takeaway
yield pauses and freezes the entire frame.
Calling the generator function does NOT run it.
Never expect side effects from the call — only from iteration.

Real-World Pattern — Processing Large Log Files

Log files are the textbook generator use case because they're naturally sequential and can grow into the gigabytes. Loading a 10 GB file into a list will crash most systems, but a generator pipeline handles it with a constant memory footprint. The pattern involves 'pipelining' where each step is a generator that pulls from the previous one, ensuring only one line of data exists in RAM at any given time.

By decoupling the reading, filtering, and parsing logic into separate generator functions, you create a modular, production-grade ETL (Extract, Transform, Load) system that is as readable as it is efficient.

io/thecodeforge/pipelines/log_processor.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import os

def get_log_lines(filename):
    """Generator to stream lines from a file."""
    with open(filename, 'r') as f:
        for line in f:
            yield line.strip()

def filter_errors(lines):
    """Generator to filter for ERROR status."""
    for line in lines:
        if "ERROR" in line:
            yield line

def parse_details(error_lines):
    """Generator to extract specific error messages."""
    for line in error_lines:
        yield line.split(" : ")[-1]

# Building the Pipeline (No execution yet)
# raw_log = get_log_lines("production_log.txt")
# errors = filter_errors(raw_log)
# final_report = parse_details(errors)

# for msg in final_report:
#     print(f"Critial Alert: {msg}")
Output
# Scalable O(1) memory usage regardless of file size.
Pro Tip:
This pipeline pattern scales from 100 lines to 100 million lines with identical memory usage, because at any instant only one line is alive across the entire chain. In production, replace the list with open('server.log') and you can stream a 10 GB log file with under 1 MB of RAM.
Production Insight
Common failure: someone converts a generator pipeline to a list for debugging, then forgets to revert. Suddenly memory goes from 1 MB to 11 GB.
Rule: never materialise in production. If you must debug, use itertools.islice to sample the first few items.
Key Takeaway
Generator pipelines keep memory at O(1) by design.
One line in flight at a time — regardless of input size.
Don't break the laziness by calling list() in production.

Memory Usage Comparison: List vs Generator

One of the most compelling reasons to use generators is the dramatic difference in memory consumption. A list stores every element in contiguous memory. A generator stores nothing — it computes each element on demand and discards it after yielding. This comparison table crystallizes the practical trade-offs for common Python workloads.

For a dataset of 10 million integers, a list would consume roughly 80 MB (8 bytes per int * 10M + list overhead). A generator requires just 112 bytes — the size of the generator object itself. The speed difference is negligible for iteration (generators add about 40ns per yield), but the memory savings are enormous.

The table below summarizes the key differences for production decision-making:

io/thecodeforge/generators/memory_comparison.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import sys
import tracemalloc

N = 10_000_000

# Tracemalloc to measure peak memory
tracemalloc.start()

# List version
eager = [i for i in range(N)]
current, peak = tracemalloc.get_traced_memory()
print(f"List: Current = {current / 1024**2:.2f} MB, Peak = {peak / 1024**2:.2f} MB")
tracemalloc.stop()

tracemalloc.start()
# Generator version
lazy = (i for i in range(N))
# Simulate consumption without storing
for _ in lazy:
    pass
current, peak = tracemalloc.get_traced_memory()
print(f"Generator: Current = {current / 1024**2:.2f} MB, Peak = {peak / 1024**2:.2f} MB")
tracemalloc.stop()
Output
List: Current = 762.94 MB, Peak = 762.94 MB
Generator: Current = 0.00 MB, Peak = 0.34 MB
Reality Check:
These numbers scale linearly. A list of 100 million integers would need ~7.6 GB — enough to OOM a typical server. The same generator still uses a few hundred KB. This is why generators are the default choice for any large or unbounded data source.
Production Insight
In production monitoring systems, we've seen teams accidentally materialise a generator by calling sorted() or max() on it. These functions consume the entire generator into a list internally. Always check the documentation: if the function returns a list, it materialises. Prefer functions that accept iterators (like heapq.nlargest) or build your own streaming aggregators.
Key Takeaway
Lists hold everything in memory (O(n)). Generators hold nothing (O(1)).
For large datasets, generators prevent out-of-memory errors.
Materialise only when you must: sorting, indexing, or multiple passes.

Advanced Mechanics: Infinite Streams and .send()

Because generators are lazy, they are the only way to represent infinite sequences. A while True loop inside a generator isn't a bug—it's a feature. Since the function pauses at every yield, it will never hang your CPU; it simply waits for the caller to request the next value. Furthermore, the .send() method allows you to push data into the generator, effectively turning it into a coroutine for two-way communication.

io/thecodeforge/generators/infinite_stream.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def infinite_fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

def tally_tracker():
    """Receives values and yields the current sum."""
    total = 0
    while True:
        val = yield total
        if val is not None:
            total += val

# Infinite usage
fib = infinite_fibonacci()
print([next(fib) for _ in range(5)])

# Two-way usage
stats = tally_tracker()
next(stats) # Prime the generator
print(f"Total after 10: {stats.send(10)}")
print(f"Total after 25: {stats.send(25)}")
Output
[0, 1, 1, 2, 3]
Total after 10: 10
Total after 25: 35
Interview Gold:
Interviewers love asking 'how would you generate an infinite sequence in Python without running out of memory?' The answer is a generator with a while True loop. The key insight is that yield suspends execution, so the infinite loop never actually spins — it only advances one step per next() call.
Production Insight
Using .send() in production is risky — it's easy to forget to prime the generator with next() first, causing a TypeError: can't send non-None value to a just-started generator.
Rule: always call next() once after creating a .send()-based generator, or wrap initialization in a factory.
Key Takeaway
Infinite sequences are natural with generators.
.send() enables coroutine-style two-way communication.
Always prime a send-based generator with an initial next() call.

yield from — Generator Delegation Made Simple

When you have nested generators, you could write a for loop to yield all items from a sub-generator. But yield from does it cleaner and faster. It delegates to another generator (or any iterable) and yields each item as if it came from the outer generator. It also propagates StopIteration and handles .send() and .throw() correctly — something a for loop doesn't do.

Use yield from when you need to flatten nested data, compose generators, or build recursive generators. It's the unsung hero of lazy pipeline designs.

io/thecodeforge/generators/yield_from_demo.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def sub_gen():
    yield "A"
    yield "B"

def main_gen():
    yield "Start"
    yield from sub_gen()  # delegates to sub_gen
    yield "End"

for item in main_gen():
    print(item)

# Recursive example: flatten nested lists lazily
def flatten(nested):
    for item in nested:
        if isinstance(item, list):
            yield from flatten(item)  # recurse through sublists
        else:
            yield item

nested_list = [1, [2, [3, 4], 5], 6]
flat = flatten(nested_list)
print(list(flat))  # [1, 2, 3, 4, 5, 6]
Output
Start
A
B
End
[1, 2, 3, 4, 5, 6]
Clean vs Loopy:
Compare: 'for x in sub: yield x' vs 'yield from sub'. They produce the same values, but yield from is faster, handles send/throw correctly, and keeps your code clean. Use it whenever you're yielding all items from another generator.
Production Insight
A performance trap: using yield from inside a tight loop that calls it repeatedly (e.g., for each row in a CSV). Each delegation has overhead of ~100ns. For billions of rows, that adds up.
Rule: use yield from for readability; if profiling shows it's a bottleneck, inline the sub-generator logic.
Key Takeaway
yield from delegates to another generator cleanly.
It propagates send/throw — for loops don't.
Use it for readability, but watch for micro-overhead in hot loops.

yield from for Recursive and Nested Generators

While the basic yield from works for simple delegation, its real power emerges when you need to recursively traverse deeply nested structures. Consider a file system tree, a JSON object with arbitrary nesting, or a game tree. A generator that recursively yields from sub-generators lets you produce a flat stream of elements without building intermediate lists.

The recursive pattern works because each call to yield from flatten(...) creates a new generator that yields items one by one. Python's call stack pushes frames for each level of nesting, but only one value exists at a time. This is a textbook example of lazy recursion: you can flatten a tree of any depth without running out of memory (though you can hit recursion depth limits for very deep trees).

For production use, combine this with itertools.islice or itertools.takewhile to limit output when you only need a subset of the nested data.

io/thecodeforge/generators/recursive_yield_from.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import os

# Recursively walk a directory tree, yielding file paths lazily
def walk_files(root):
    for entry in os.scandir(root):
        if entry.is_dir():
            yield from walk_files(entry.path)
        else:
            yield entry.path

# Usage: only one file path in memory at a time
# for file_path in walk_files("/var/log"):
#     process(file_path)

# Another pattern: nested JSON traversal
def extract_strings(data):
    if isinstance(data, dict):
        for _, v in data.items():
            yield from extract_strings(v)
    elif isinstance(data, list):
        for item in data:
            yield from extract_strings(item)
    elif isinstance(data, str):
        yield data

nested = {
    "a": "hello",
    "b": {"c": ["world", "foo"]},
    "d": [1, {"e": "bar"}]
}
print(list(extract_strings(nested)))
# ['hello', 'world', 'foo', 'bar']
Output
['hello', 'world', 'foo', 'bar']
Recursion Depth Warning:
Python’s default recursion limit is 1000. If your nested structure exceeds that, use an iterative approach with an explicit stack instead. For filesystem traversal, os.scandir limits depth implicitly, but deep directories can cause RecursionError.
Production Insight
In production log aggregation, we've used recursive yield from to flatten nested JSON log entries from multiple sources. The key advantage: we can stream millions of events without buffering. A common mistake is to call list() on the generator to get all values, which defeats the purpose. Always consume lazily with a for loop.
Key Takeaway
Recursive yield from flattens nested structures lazily.
Memory stays O(1) — one element at a time.
Watch recursion depth; for extreme depth, use iterative stack.

Generators vs Lists vs Iterators — Knowing When to Use Each

The honest answer to 'when should I use a generator?' is: whenever you don't need all the values at once, or whenever you might not need all of them at all. If you need to sort, reverse, index by position, or pass the same sequence to multiple consumers, use a list — you need all values materialised. If you're transforming or filtering a sequence and consuming it exactly once from start to finish, a generator is almost always the better choice.

One critical difference that surprises people: generators are single-use. Once exhausted, they're done — calling iter() on them again doesn't restart them. A list can be iterated as many times as you like. This is the most common source of subtle bugs with generators in production code.

Custom iterator classes (with __iter__ and __next__) give you the same lazy behaviour as generators but with more control — you can maintain state, support multiple independent iterations, or define a length. Generators are the shortcut for the 80% case where you just need simple, one-shot lazy iteration.

io/thecodeforge/generators/exhaustion_demo.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_gen():
    yield 1
    yield 2

my_gen = get_gen()

# Pass 1
print(list(my_gen)) # [1, 2]

# Pass 2
print(list(my_gen)) # [] - The generator is empty!

# Pass 3: Re-calling the function creates a FRESH generator
fresh_gen = get_gen()
print(list(fresh_gen)) # [1, 2]
Output
[1, 2]
[]
[1, 2]
Watch Out:
Passing a generator to a function that secretly iterates it fully (like sorted(), max(), list(), or any()) exhausts it silently. Any subsequent attempt to iterate that generator produces nothing. If you need to reuse values, call list() on the generator once and store the result.
Production Insight
In production log processing, we've seen a generator passed to both an error filter and a summary counter. The filter exhausted it, so the counter returned zero. No error — just wrong metrics for hours.
Rule: if you need two pipelines, materialise once with list() and then operate on the list. Or redesign to merge the two consumers into one pass.
Key Takeaway
Generators are single-use — iterate them once.
Need multiple passes? Call list() once, then use the list.
Custom iterators give you reusable lazy objects — use them for complex state.

Advanced Generator Methods: .send() and .throw()

Beyond simple iteration, generators support two advanced methods that turn them into two-way communication channels: .send() and .throw(). These are often overlooked but essential for building coroutine-like patterns, cooperative multitasking, and generator-based pipelines with error handling.

.send(val) resumes the generator and passes a value into it, which becomes the result of the yield expression inside the generator. This lets you inject data from outside. .throw(type, value, traceback) raises an exception at the point where the generator was paused. The generator can catch it (via try/except around the yield) and yield another value, or let it propagate to terminate the generator.

A common use case for .throw() is to signal a generator to clean up or stop early, akin to a cancel signal. For pipelines, you can throw an exception into the middle of a chain to abort processing without manually draining the generator.

io/thecodeforge/generators/send_throw.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# .send() example
from typing import Generator

def accumulator() -> Generator[float, float, None]:
    total = 0.0
    while True:
        increment = yield total
        if increment is not None:
            total += increment

acc = accumulator()
next(acc)  # prime
print(acc.send(10.5))  # 10.5
print(acc.send(3.2))   # 13.7


# .throw() example – stopping a generator gracefully
def produce_items():
    try:
        for i in range(1000):
            yield i
    except GeneratorExit:
        print("Cleanup: closing generator")
    except Exception as e:
        print(f"Caught exception: {e}")

producer = produce_items()
print(next(producer))  # 0
print(next(producer))  # 1
producer.throw(RuntimeError, "cancel")
# output: Caught exception: cancel
# generator ends

# Without catching, .throw() propagates
def simple_gen():
    yield 1
    yield 2

g = simple_gen()
next(g)
try:
    g.throw(ValueError, "test")
except ValueError:
    print("ValueError propagated")
Output
10.5
13.7
0
1
Caught exception: cancel
ValueError propagated
Priming Gotcha:
You must call next() on a generator before using .send() or .throw(). The first call sets up the generator at its first yield point. Forgetting this raises TypeError: can't send non-None value to a just-started generator.
Production Insight
In production, .throw() is useful for aborting a long-running generator pipeline (e.g., when a timeout occurs). However, catching GeneratorExit in the generator is delicate — if you catch it and yield again, Python will raise RuntimeError. Use .send() for data injection only when you need push-based patterns; otherwise, prefer passing data through function arguments to keep pipelines predictable.
Key Takeaway
.send() injects values into generators.
.throw() injects exceptions.
Always prime with next() before using these methods.
Use .throw() sparingly — it can make control flow confusing.
● Production incidentPOST-MORTEMseverity: high

The Silent Empty Log Report — Generator Exhaustion in Production

Symptom
A log processing system that had been running fine for weeks suddenly output zero results. Log lines were being read, but the final alert report was empty.
Assumption
The developer assumed the generator object could be iterated multiple times, just like a list. They passed the same generator to two consumers: one filtered errors, the other counted total lines.
Root cause
The generator was passed to filter_errors() which iterated it fully. When the count function later tried to iterate the same generator, it received nothing — StopIteration was already raised. No error was thrown; the for loop just didn't execute.
Fix
Change the pipeline to either materialize the data once with list(raw_lines) if both passes are needed, or restructure to avoid double consumption. In this case, counting should happen side-effect-free in the same pass as filtering.
Key lesson
  • Generators are single-use. Passing one to a function that iterates it fully exhausts it silently.
  • If multiple consumers need the same data, call list() on the generator once and store the result.
  • Never assume iteration order or count — verify with a small test before deploying any generator pipeline.
Production debug guideSymptom → Action. Diagnose silent failures fast.4 entries
Symptom · 01
Generator pipeline returns no results even though input data exists
Fix
Check if the generator was already consumed earlier. Wrap in list() at the pipeline start and compare output. Add a debug print('Consumed by', func.__name__) in each consumer function.
Symptom · 02
Memory spikes when processing large files
Fix
Look for an accidental list() call inside the pipeline. For example, list(lines) in a filter function materialises everything. Replace with lazy chaining.
Symptom · 03
Generator function side effects (like writing to a file) never happen
Fix
Verify you actually iterate the generator. print(type(obj)) — if it's a generator, you forgot to call next(). Use for value in generator: or list(generator) to trigger execution.
Symptom · 04
Generator runs forever (hangs) in a for loop
Fix
Your generator has an infinite loop with no terminating condition. Check that the while True loop has a break condition or that the caller limits iterations. Use itertools.islice(generator, n) to cap consumption.
★ Quick Debug Cheat Sheet for GeneratorsStop wasting time on generator quirks. These are the three most common symptoms and exactly what to do.
Generator returns empty when you expect values
Immediate action
Check if the generator was already consumed by another consumer.
Commands
print(type(my_gen)) — confirm it's a generator object, not a function.
print(list(my_gen)) — if empty, it's exhausted. Recreate by calling the generator function again.
Fix now
Store a list if multiple passes needed: data = list(my_gen_func()) and then work with data.
Pipeline using generators is slower than expected+
Immediate action
Check if you're accidentally materialising intermediate results.
Commands
Add print(len(list(intermediate))) at each stage to see memory usage.
Profile with timeit.timeit(''.join(chunk) for chunk in chunks) vs [chunk for chunk in chunks].
Fix now
Ensure every function in the chain returns a generator (uses yield), not a list.
Generator body code runs at import time?+
Immediate action
No, it doesn't. That's the bug: you called the function expecting it to execute.
Commands
print('called') inside the generator — does it print? Only if you iterate.
Check for accidental () call: my_gen_func() returns generator, my_gen_func does nothing.
Fix now
Wrap the generator call in list() or a for loop to trigger body execution.
Generator vs List vs Custom Iterator
Feature / AspectGenerator (yield)ListCustom Iterator Class
Memory usageO(1) — constant, holds 1 value at a timeO(n) — holds all n values simultaneouslyO(1) — same as generator
Speed to first valueInstant — starts on first next() callSlower — must compute all values before you get anyInstant — same as generator
Reusable (multi-pass)No — exhausted after one full iterationYes — iterate as many times as neededYes — if __iter__ returns a new iterator each time
Supports indexing (list[2])No — forward-only, no random accessYes — full index and slice supportNo — forward-only unless you implement __getitem__
Works with infinite sequencesYes — naturally handles unbounded outputNo — would require infinite memoryYes — same as generator
Complexity to createMinimal — just add yield to a functionMinimal — [expr for x in iterable]Moderate — define class with __iter__ and __next__
Best forLarge files, streams, pipelines, one-shot transformsSmall-medium data needing sort, index, or reuseWhen you need reusable lazy behavior with additional methods

Key takeaways

1
yield pauses a function and freezes its entire execution frame
local variables, loop state, everything — until the next next() call resumes it from exactly where it stopped.
2
Generators are single-use
once the last value has been yielded, the generator object is permanently exhausted. Iterating it again produces nothing and raises no error — a silent bug if you're not aware.
3
The lazy pipeline pattern
chaining generator functions so each pulls from the previous on demand — keeps memory usage flat at O(1) regardless of dataset size, making it the go-to architecture for log processing, ETL, and data streaming.
4
Use a generator when you consume a sequence once from start to finish; use a list when you need sorting, indexing, random access, or multiple passes over the same data.
5
yield from delegates to another generator and propagates send/throw correctly
use it for clean composability.
6
Always prime a .send()-based generator with an initial next() call to avoid TypeError.

Common mistakes to avoid

3 patterns
×

Expecting a generator function to run on call

Symptom
You call my_gen_func() to trigger side effects (like printing) and nothing happens, or you print the return value and see '<generator object>' instead of your data.
Fix
Remember the function body doesn't execute until you iterate. Wrap in list() or use a for loop to actually run it, e.g. list(my_gen_func()) or next(my_gen_func()).
×

Iterating an exhausted generator and getting no error

Symptom
Your second for loop over the same generator variable silently produces nothing, no exception, no warning, just zero iterations.
Fix
Generators raise StopIteration internally and for loops catch it silently. If you need multiple passes, store the results with results = list(my_generator()) and iterate results repeatedly, or call the generator function again to get a fresh generator object.
×

Using a generator expression where you immediately need all values anyway

Symptom
You write total = sum((x2 for x in big_list)) then immediately also need max((x2 for x in big_list)), iterating big_list twice with two separate generators when one pass would do.
Fix
If you need multiple aggregations over the same computed values, materialise once with squares = [x**2 for x in big_list] then compute sum(squares) and max(squares). The laziness of a generator only helps when you consume the sequence once.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between a generator function and a regular functi...
Q02SENIOR
How would you use a generator to process a 50 GB CSV file on a machine w...
Q03SENIOR
If I convert a generator expression to a list comprehension, the results...
Q04SENIOR
Explain the internal mechanism of 'StopIteration' and how Python's for-l...
Q05SENIOR
What is 'Generator Delegation' and how do you use 'yield from' to flatte...
Q06SENIOR
Can you return a value in a generator? If so, what happens to that value...
Q01 of 06JUNIOR

What is the difference between a generator function and a regular function, and what happens to the execution frame when yield is encountered?

ANSWER
A regular function uses return and discards its local state after execution. A generator function uses yield. When yield is hit, the function pauses, the yielded value is sent to the caller, and the entire execution frame (local variables, loop counters, instruction pointer) is frozen. The next time next() is called, execution resumes from right after the yield. The generator function returns a generator object when called, not a value.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the difference between yield and return in Python?
02
Can a Python generator function use both yield and return?
03
Are Python generator expressions the same as list comprehensions?
04
What is 'yield from' and when should I use it?
🔥

That's Functions. Mark it forged?

5 min read · try the examples if you haven't

Previous
Decorators in Python
5 / 11 · Functions
Next
Closures in Python