Comprehensions & Generators
15 questions found
How do you write a basic list comprehension, and how does it compare to an equivalent for loop?
Beginner
A list comprehension `[expr for item in iterable]` builds a new list in one concise expression, evaluating 'expr' for each item — functionally equivalent to a for loop that appends to a list, but generally more readable and slightly faster since it avoids repeated append() method calls.
squares = [n * n for n in range(5)]
print(squares) # [0, 1, 4, 9, 16]
# Equivalent for loop:
squares2 = []
for n in range(5):
squares2.append(n * n)
Real-world example
Quickly transforming a list of raw values (like strings) into a list of processed values (like their uppercase forms).
Common follow-ups: How do you add a conditional filter to a list comprehension?
Data Types & Structures
How do you add a filtering condition to a list comprehension using 'if'?
Beginner
Add an 'if condition' clause after the 'for' clause; only items for which the condition is truthy are included in the resulting list, exactly like a for loop with a nested 'if' before the append.
evens = [n for n in range(10) if n % 2 == 0]
print(evens) # [0, 2, 4, 6, 8]
Real-world example
Filtering a list of numbers or objects down to just the ones matching a specific condition, in a single expression.
Common follow-ups: How would you write an if/else (ternary) expression INSIDE the comprehension, versus a filtering 'if'?
Data Types & Structures
What is a generator function, and how does 'yield' make it different from a regular function?
Beginner
A generator function contains at least one 'yield' statement; calling it doesn't run the function body immediately — it returns a generator object, and the body only executes incrementally, one step at a time, each time you request the next value (via next() or a for loop).
def count_up_to(n):
i = 1
while i <= n:
yield i
i += 1
for number in count_up_to(3):
print(number) # 1, 2, 3
Real-world example
Lazily producing a sequence of values one at a time instead of building and returning a full list upfront.
Common follow-ups: What exception is raised internally when a generator is exhausted and next() is called again?
Iterators & the Iterator Protocol
How do dict comprehensions and set comprehensions differ in syntax from list comprehensions?
Intermediate
A dict comprehension uses curly braces with a 'key: value' pair (`{k: v for ...}`), producing a dictionary; a set comprehension uses curly braces with just an expression (`{expr for ...}`, no colon), producing a set with automatically deduplicated values — both otherwise support the same for/if clauses as list comprehensions.
squares_dict = {n: n * n for n in range(5)}
print(squares_dict) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
unique_lengths = {len(word) for word in ["cat", "dog", "bird", "ox"]}
print(unique_lengths) # {3, 4, 2}
Real-world example
Building a lookup dictionary from a list of objects (keyed by ID), or a set of unique values derived from a list.
Common follow-ups: How would you write a comprehension that produces a dict from two PARALLEL lists using zip()?
Data Types & Structures
What is a generator expression, and how does it differ syntactically and behaviorally from a list comprehension?
Intermediate
A generator expression uses parentheses `(expr for item in iterable)` instead of square brackets, and produces a LAZY generator object (computing values one at a time on demand) rather than an eagerly-built list — using far less memory for large or infinite sequences since it never materializes the whole sequence.
sum_of_squares = sum(n * n for n in range(1_000_000)) # generator expression: no intermediate list built
# Wasteful alternative: builds a full 1,000,000-element list just to sum it
sum_of_squares2 = sum([n * n for n in range(1_000_000)])
Real-world example
Computing a sum, max, or any() result over a large sequence without ever materializing the full intermediate list in memory.
Common follow-ups: Can a generator expression be passed directly as a function's sole argument without extra parentheses, like sum(x for x in range(10))?
Memory Management & Garbage Collection
How do you write a nested comprehension, like flattening a list of lists into a single flat list?
Intermediate
Chain multiple 'for' clauses in the SAME order you'd nest equivalent for loops — the outer loop comes first, inner loops follow, letting you flatten nested structures or produce combinations in a single comprehension expression.
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened = [num for row in matrix for num in row]
print(flattened) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
Real-world example
Flattening a 2D grid or matrix of values into a single flat list for further processing.
Common follow-ups: At what point does a nested comprehension become hard enough to read that a regular for loop is clearer?
Data Types & Structures
How do you send a value INTO a generator using the .send() method, and how does this differ from just calling next()?
Intermediate
generator.send(value) resumes the generator's execution and makes 'value' become the RESULT of the 'yield' expression that's currently paused — unlike next() (equivalent to send(None)), send() lets you communicate data back INTO the generator's ongoing execution, enabling coroutine-like two-way communication.
def echo():
while True:
received = yield
print(f"Received: {received}")
gen = echo()
next(gen) # prime the generator to the first yield
gen.send("hello") # prints "Received: hello"
Real-world example
Building a simple coroutine-style consumer that processes values pushed to it one at a time, a pattern predating async/await.
Common follow-ups: Why must you call next() (or send(None)) once to 'prime' a generator before you can send() a real value into it?
Functions & Scope
How does 'yield from' delegate iteration to a sub-generator, and what does it return as its own expression value?
Advanced
'yield from sub_generator' fully delegates yielding to the sub-generator, forwarding each of its values as if they were yielded directly by the outer generator — AND its expression evaluates to whatever the sub-generator eventually returns (via a 'return' statement), letting you compose generators while still capturing a final result.
def inner():
yield 1
yield 2
return "inner done"
def outer():
result = yield from inner()
print(result) # "inner done", printed after inner's values are yielded
yield 3
list(outer()) # yields 1, 2, 3; prints "inner done" in between
Real-world example
Composing a large generator out of smaller, reusable generator building blocks, similar to function composition.
Common follow-ups: How does 'yield from' also automatically forward .send() and .throw() calls into the delegated sub-generator?
Iterators & the Iterator Protocol
How does generator.throw() let you inject an exception into a paused generator, and how would the generator handle it?
Advanced
generator.throw(exception) resumes the generator by raising the given exception at the currently-paused yield point INSIDE the generator's frame — if the generator has a try/except around that yield, it can catch and handle it (potentially yielding a different value); otherwise, the exception propagates out to the caller of throw().
def resilient_gen():
try:
while True:
yield "working"
except ValueError:
yield "recovered from error"
gen = resilient_gen()
next(gen) # "working"
print(gen.throw(ValueError)) # "recovered from error"
Real-world example
Building a generator-based state machine or task runner that can gracefully handle externally-injected error conditions.
Common follow-ups: What does generator.close() do differently from throw(), in terms of what exception it raises internally?
Exception Handling
How does lazy evaluation in a chained sequence of generator expressions actually process elements, one at a time end-to-end, without intermediate lists?
Advanced
Each generator expression only pulls from its SOURCE generator when IT is asked for its own next value, propagating the request all the way back to the original iterable — meaning a chain of generator expressions processes ONE element completely through the entire pipeline before moving to the next, rather than processing each stage across the whole dataset before the next stage begins.
data = range(1_000_000)
pipeline = (x * 2 for x in (y + 1 for y in data if y % 2 == 0))
first_five = [next(pipeline) for _ in range(5)]
# Only the first ~10 elements of 'data' were ever actually touched, not the whole million
Real-world example
Processing a huge dataset through a multi-stage transformation pipeline while only ever holding one element's worth of state at each stage.
Common follow-ups: How does this pull-based, element-at-a-time evaluation order differ from how a similarly-chained list comprehension would behave?
Memory Management & Garbage Collection