Iterators & Generators β Python Questions with Answers
Start with the study notes on Iterators & Generators (definitions, examples and a quick revision), then work through 30 practice questions, each with the correct answer and a clear explanation of why it is right. When you are ready, take this topic as an interactive quiz to test yourself.
3 easy Β· 18 medium Β· 9 hard Β· Part of Python: Master Python from basics to advanced - 20 topics.
Topic overview
Iterators and Generators β values one at a time
1. Iterables and iterators
An iterable can be looped over: lists, strings, dicts, ranges.
An iterator is the object that hands out the values one at a time with next(). iter(x) gets one from an iterable.
When an iterator runs out, next() raises StopIteration.
nums = [1, 2]
it = iter(nums)
print(next(it))
print(next(it))
print(next(it, "done")) # a default instead of StopIteration
Output1
2
done
Remember: A for loop calls iter() once, then next() again and again, and stops quietly at StopIteration.
2. Generator functions and yield
A function that contains yield is a generator function. Calling it runs nothing: it returns a generator object. Each next() runs the body up to the next yield, pauses there, and remembers everything.
def count_up(n):
for i in range(1, n + 1):
yield i
g = count_up(3)
print(next(g))
print(list(g)) # the rest
A list stores every value at once. A generator produces each value only when asked, so it can handle huge or even endless data in very little memory.
from itertools import islice
def naturals():
n = 1
while True: # endless, but harmless
yield n
n += 1
print(list(islice(naturals(), 5)))
def fib():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
print(list(islice(fib(), 8)))
Output[1, 2, 3, 4, 5]
[0, 1, 1, 2, 3, 5, 8, 13]
4. Generators are one-pass
Once a generator is used up, it stays empty. Call the generator function again for a fresh one, or save the values in a list if you need two passes.
def gen():
yield 1
yield 2
g = gen()
print(list(g))
print(list(g))
print(list(gen())) # a new generator
Output[1, 2]
[]
[1, 2]
Remember: Even x in g uses up values from a generator.
5. Generator expressions and pipelines
(expr for x in items) is a one-line generator. Chaining them makes a pipeline: each item flows through every step one at a time.
lines = ["ok", "ERROR disk", "ok", "ERROR net"]
errors = (l for l in lines if "ERROR" in l)
short = (l.split()[1] for l in errors)
print(list(short))
print(sum(x * x for x in range(4)))
Output['disk', 'net']
14
6. yield from
yield from iterable hands out every value of another iterable. It is very useful in recursive generators.
def all_items():
yield from [1, 2]
yield from "ab"
print(list(all_items()))
Output[1, 2, 'a', 'b']
7. Writing an iterator class
A class becomes an iterator by defining __iter__ (which returns self) and __next__ (which returns the next value, or raises StopIteration). Generators do all of this for you, which is why they are usually preferred.
class Countdown:
def __init__(self, n):
self.n = n
def __iter__(self):
return self
def __next__(self):
if self.n <= 0:
raise StopIteration
self.n -= 1
return self.n + 1
print(list(Countdown(3)))
Output[3, 2, 1]
8. Lazy built-ins and itertools
range, map, filter and zip are lazy too. The itertools module has ready-made iterator tools: count, cycle, chain and islice.
from itertools import chain, count, islice
m = map(str.upper, ["a", "b"])
print(m.__class__.__name__, list(m))
print(list(chain([1, 2], [3])))
print(list(islice(count(10), 3)))
print(10**11 in range(10**12)) # instant: computed, not searched
Outputmap ['A', 'B']
[1, 2, 3]
[10, 11, 12]
True
9. Common misconceptions
Misconception
Correct understanding
Calling a generator function runs its body.
It returns a generator; the body runs only on next() or in a loop.
You can loop over a generator twice.
Generators are one-pass; after use they are empty.
yield works like return.
return ends the function; yield pauses it and resumes later.
g[0] gives a generator's first value.
Generators can't be indexed; use next(g).
range(10**12) builds a huge list.
range is lazy and stores only start, stop and step.
10. Quick revision
Iterable
Something you can loop over.
Iterator
Hands out values one by one with next().
StopIteration
Raised when an iterator is used up.
yield
Hands out a value and pauses the function.
Generator
A lazy iterator made by a yield function.
Generator expression
(expr for x in items).
yield from
Hands out all values of another iterable.
itertools
Module of iterator tools.
11. Test your understanding
What happens when next() is called on a used-up iterator?
Show answer
StopIteration is raised (unless a default is given).
What makes a function a generator function?
Show answer
It contains yield.
What is the key difference between yield and return?
Show answer
yield pauses and resumes; return ends the function.
Why does a generator save memory?
Show answer
It produces values one at a time instead of storing them all.
What does list(g) give the second time for the same generator?
Show answer
[] (it is already used up).
Complete the Fibonacci step after yield a.
Show answer
a, b = b, a + b
How do you take the first 5 values of an endless generator?
Show answer
itertools.islice(gen(), 5)
Which two methods make a class an iterator?
Show answer
__iter__ and __next__.
Key takeaway: Generators hand out values one at a time with yield, so they handle huge or endless data in little memory, but each one can only be used once.
Notes written by Velnov. Every code example with an output was run in Python 3 to check that the output shown is exactly what Python prints.
def count_up(n):
for i in range(1, n + 1):
yield i
g = count_up(3)
print(list(g)) # [1, 2, 3]
A It contains the yield keyword
B It's named with gen_
C It returns a list
D It uses async
Answer: A β It contains the yield keyword
One yield anywhere in the body changes everything: calling the function no longer runs it - it returns a generator object that produces values lazily as you iterate.
yieldGenerators
Q24.easy
Which is the LAZY equivalent of the list comprehension [x*2 for x in nums]?
A (x*2 for x in nums)
B {x*2 for x in nums}
C lazy[x*2 for x in nums]
D [x*2 async for x in nums]
Answer: A β (x*2 for x in nums)
Same syntax, round brackets: a generator expression. Choose it whenever you'll iterate once (feeding sum, join, any...); choose the list when you need indexing, len, or multiple passes.
Generator expressionsChoosing
Q27.easy
iter([1, 2, 3]) returns:
A A list_iterator object ready for next() calls
B The same list
C The first element
D A tuple
Answer: A β A list_iterator object ready for next() calls
iter() asks the iterable for its iterator - the object for loops actually drive. You rarely call it yourself, but it's what happens inside every for.
iter()
Medium questions (18)
Q1.medium
What is the difference between an ITERABLE and an ITERATOR?
A An iterable can PRODUCE an iterator (lists, strings); an iterator is the object that actually yields values one by one via next()
B They are exact synonyms
C Iterators are always faster
D Iterables only hold numbers
Answer: A β An iterable can PRODUCE an iterator (lists, strings); an iterator is the object that actually yields values one by one via next()
A list is iterable - you can loop it many times; each loop asks it for a fresh ITERATOR (via iter()), and the iterator is what next() pulls values from until it's exhausted.
iter()next()
Q2.medium
What does next(it) do when the iterator has NO values left?
it = iter([1])
next(it) # 1
# next(it) # StopIteration!
next(it, "done") # 'done' - default avoids the error
A Raises StopIteration
B Returns None forever
C Restarts from the beginning
D Returns -1
Answer: A β Raises StopIteration
Exhausted iterators raise StopIteration - the signal that iteration is over. for-loops catch it silently; manual next() calls will see the exception.
StopIterationnext() default
Q5.medium
What is the KEY difference between yield and return?
def demo():
print("start")
yield 1
print("resumed")
yield 2
g = demo()
next(g) # prints start, gives 1
next(g) # prints resumed, gives 2
A return exits for good; yield PAUSES the function, remembers everything, and resumes on the next next()
B yield is just a faster return
C return sends multiple values
D No difference
Answer: A β return exits for good; yield PAUSES the function, remembers everything, and resumes on the next next()
yield hands out a value and freezes the function mid-flight - locals, loop position, everything preserved. The next next() resumes right after the yield. return (or falling off the end) raises StopIteration.
yieldFunction state
Q6.medium
What is the output?
def gen():
yield "a"
yield "b"
print(gen())
A <generator object gen at 0x...> - calling it produces a generator, not values
B a
C ['a', 'b']
D ab
Answer: A β <generator object gen at 0x...> - calling it produces a generator, not values
Calling a generator function runs NONE of its body - it returns a generator object. Values appear only when iterated: list(gen()), next(g), or a for loop.
Generator objectsLazy evaluation
Q7.medium
Why is a generator more MEMORY-EFFICIENT than returning a list?
def read_big_file(path):
with open(path) as f:
for line in f:
yield line.strip() # one line at a time
A It produces one value at a time on demand - never storing the whole sequence
B It compresses the list
C It uses the GPU
D It isn't - lists are always better
Answer: A β It produces one value at a time on demand - never storing the whole sequence
A list of 10 million items sits fully in RAM. A generator holds just the current state (a few variables) and manufactures each value when asked - processing unlimited data in constant memory.
Memory efficiencyStreaming
Q10.medium
Which two methods must a CLASS implement to be its own iterator?
A __iter__ (returns self) and __next__ (returns the next value or raises StopIteration)
B __loop__ and __stop__
C __for__ and __in__
D next() and prev()
Answer: A β __iter__ (returns self) and __next__ (returns the next value or raises StopIteration)
The iterator protocol: __iter__ makes the object usable in for loops; __next__ supplies each value and raises StopIteration when done. Generators implement all this automatically - which is why they're usually preferred.
Iterator protocol__next__
Q11.medium
Why does range(10**12) create INSTANTLY without eating memory?
r = range(10**12) # instant
print(len(r)) # 1000000000000
print(10**11 in r) # True - computed, not searched
A range is lazy - it stores only start/stop/step and computes values on demand
B Python compresses the numbers
C It secretly fails
D Modern RAM is big enough
Answer: A β range is lazy - it stores only start/stop/step and computes values on demand
range objects are not lists - they're compact lazy sequences (3 numbers + math). Values materialize only during iteration, and 'x in range(...)' is even computed arithmetically without looping.
A Lazy iterator objects - wrap in list() to see all values
B Lists, like Python 2
C Tuples
D Strings
Answer: A β Lazy iterator objects - wrap in list() to see all values
map/filter/zip all went lazy in Python 3 - they yield on demand and print as <map object ...>. list() forces them. They're also one-pass, like generators.
map()filter()Python 2 vs 3
Q13.medium
What is the output?
def squares(n):
for i in range(n):
yield i * i
print(sum(squares(4)))
A 14
B 30
C 6
D Error
Answer: A β 14
squares(4) yields 0, 1, 4, 9; sum() consumes them as they're produced: 14. Feeding generators straight into sum/max/min/any/all is the idiomatic pattern.
sum()Generator consumption
Q15.medium
sum(x*x for x in nums) - why no extra brackets inside sum()?
total = sum(x*x for x in nums) # OK
sorted((x*x for x in nums), reverse=True) # parens needed - 2 args
A A generator expression can drop its parentheses when it's the ONLY argument of a call
B It's actually a list comprehension
C sum() has special syntax
D It's a syntax error
Answer: A β A generator expression can drop its parentheses when it's the ONLY argument of a call
Normally genexps need (x*x for x in nums), but as a lone function argument the call's own parentheses suffice - clean and memory-friendly. With two arguments you'd need the inner parens back.
Generator expressionsSyntax
Q17.medium
How do you take just the FIRST value from a generator g?
g = (x*x for x in range(10))
print(next(g)) # 0
# g[0] # TypeError!
A next(g)
B g[0]
C g.first()
D g.pop()
Answer: A β next(g)
Generators support next(), not indexing - g[0] is a TypeError (no __getitem__). next(g) pulls exactly one value.
next()No indexing
Q18.medium
Trace the output ORDER:
def gen():
print("A")
yield 1
print("B")
g = gen()
print("C")
next(g)
A C then A - the body doesn't start until the first next()
B A then C
C A B C
D C A B
Answer: A β C then A - the body doesn't start until the first next()
g = gen() runs NOTHING. print("C") happens first. Only next(g) starts the body: 'A' prints, 1 is yielded, and the function pauses before 'B'. Laziness surprises everyone once.
Lazy executionTracing
Q20.medium
What does next(it, "default") do that next(it) doesn't?
match = next((x for x in nums if x > 100), None)
if match is None:
print("nothing found")
A Returns "default" instead of raising StopIteration when the iterator is exhausted
B Sets the iterator's default forever
C Skips one value
D Nothing different
Answer: A β Returns "default" instead of raising StopIteration when the iterator is exhausted
The two-argument form is exception-free iteration: perfect for 'give me the next one, or a fallback'. Great with generator-based searches: next((x for x in items if x > 5), None).
next() defaultSearch pattern
Q21.medium
Is a STRING iterable, an iterator, or both?
s = "abc"
# next(s) # TypeError
it = iter(s)
print(next(it)) # a
A Iterable only - iter("abc") produces the iterator; the string itself has no __next__
B Iterator only
C Both at once
D Neither
Answer: A β Iterable only - iter("abc") produces the iterator; the string itself has no __next__
Strings (like lists and dicts) are re-iterable containers: every for-loop gets a fresh iterator. True iterators (generators, map objects, file objects) can be looped only once.
Iterables vs iterators
Q23.medium
A file object f is an iterator. What does that mean practically?
A for line in f reads lazily, one line at a time - and a second loop over f continues from where the first stopped (or finds it exhausted)
B Files load fully into RAM on open
C You can index f[3]
D Files must be lists
Answer: A β for line in f reads lazily, one line at a time - and a second loop over f continues from where the first stopped (or finds it exhausted)
Files stream: perfect for huge logs. But being iterators, they exhaust - a second 'for line in f' reads nothing unless you f.seek(0) first.
File iterationExhaustion
Q26.medium
Why use a generator to produce test data like a million fake users?
def fake_users():
i = 0
while True:
i += 1
yield {"id": i, "name": f"user{i}"}
A Consumers can process users one at a time without a million-object list in memory - and stop early for free
B Generators create more realistic data
C Lists can't hold a million items
D No benefit
Answer: A β Consumers can process users one at a time without a million-object list in memory - and stop early for free
yield one user at a time: whoever consumes can take 10 or a million, memory stays flat, and breaking out early costs nothing (remaining users are never created).
Lazy generationEarly exit
Q28.medium
What is the output?
def gen():
yield 1
return
yield 2
print(list(gen()))
A [1] - return stops the generator; the second yield is never reached
B [1, 2]
C [1, None, 2]
D Error
Answer: A β [1] - return stops the generator; the second yield is never reached
return inside a generator ends iteration (raising StopIteration internally). Everything after it is dead code - list() collects only the 1.
return in generatorsStopIteration
Q30.medium
Rule of thumb: WHEN should you write a generator instead of returning a list?
A Large/unbounded data, streaming pipelines, or when callers may stop early - laziness pays off
B Always - lists are obsolete
C Never - lists are always simpler
D Only for numbers
Answer: A β Large/unbounded data, streaming pipelines, or when callers may stop early - laziness pays off
Small data you'll reuse: return a list. Huge files, endless streams, expensive items, early-exit searches: yield. The beauty: callers loop over both the same way.
Design choiceLazy vs eager
Hard questions (9)
Q3.hard
What does a for loop REALLY do under the hood?
for x in nums:
# for x in nums: ==
it = iter(nums)
while True:
try:
x = next(it)
except StopIteration:
break
A Calls iter(nums) once, then next() repeatedly until StopIteration
B Indexes nums[0], nums[1]... until IndexError
C Copies the list first
D Uses a while loop with a counter
Answer: A β Calls iter(nums) once, then next() repeatedly until StopIteration
for is sugar over the iterator protocol: get an iterator with iter(), pull values with next(), stop cleanly on StopIteration. That's why ANY object implementing the protocol works in a for loop.
Iterator protocolfor loops
Q8.hard
Can a generator be INFINITE?
def naturals():
n = 1
while True:
yield n
n += 1
A Yes - it only computes values as requested, so the infinite loop is harmless
B No - it would hang immediately
C Only up to 1 million
D Only with recursion
Answer: A β Yes - it only computes values as requested, so the infinite loop is harmless
Laziness makes infinity practical: each next() runs the loop exactly one step. Consumers take what they need (islice, break, zip with something finite). Lists could never do this.
Infinite generatorsitertools.islice
Q9.hard
What is the output?
def gen():
yield 1
yield 2
g = gen()
print(list(g))
print(list(g))
g = gen()
print(list(g)) # [1, 2]
print(list(g)) # [] !
print(list(gen())) # [1, 2] - new generator
A [1, 2] then [] - generators are ONE-PASS; once exhausted, they're empty forever
B [1, 2] then [1, 2]
C [1, 2] then Error
D [] then [1, 2]
Answer: A β [1, 2] then [] - generators are ONE-PASS; once exhausted, they're empty forever
The first list() drains the generator; the second finds it exhausted and gets []. To iterate again, call gen() for a FRESH generator. Silent empty results after accidental double-iteration is a classic bug.
Same trap applies to zip, map and filter objects.
One-pass iterationExhaustion
Q14.hard
The classic: complete the Fibonacci generator.
def fib():
a, b = 0, 1
while True:
yield a
______
def fib():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
from itertools import islice
print(list(islice(fib(), 8))) # [0, 1, 1, 2, 3, 5, 8, 13]
A a, b = b, a + b
B a = b + a
C b = a
D a, b = a + b
Answer: A β a, b = b, a + b
Tuple assignment advances both numbers at once: new a = old b, new b = old a+b. Yields 0, 1, 1, 2, 3, 5, 8... The most-asked generator interview question.
FibonacciTuple assignment
Q16.hard
What does yield from do?
def all_items():
yield from [1, 2]
yield from [3, 4]
def walk(tree):
yield tree.value
for child in tree.children:
yield from walk(child) # recursive delegation
A Delegates to another iterable - yielding each of ITS values in turn: 1, 2, 3, 4
B Imports yields from another file
C Yields the list objects themselves
D SyntaxError
Answer: A β Delegates to another iterable - yielding each of ITS values in turn: 1, 2, 3, 4
yield from iterable is shorthand for 'for x in iterable: yield x' - flattening sub-iterables into your generator. Essential for recursive generators (e.g. walking a tree).
yield fromDelegation
Q19.hard
What is a generator PIPELINE?
lines = open("log.txt")
errors = (l for l in lines if "ERROR" in l)
short = (l[:50] for l in errors)
A Chained lazy stages - each line flows through filter and slice one at a time; nothing processes until you iterate the last stage
B Running generators in parallel threads
C A pipe character syntax
D An error - generators can't chain
Answer: A β Chained lazy stages - each line flows through filter and slice one at a time; nothing processes until you iterate the last stage
Each genexp wraps the previous one; iterating 'short' pulls one line through the whole chain at a time. Gigabyte logs processed in kilobytes of RAM - Unix pipes, in Python.
PipelinesStreaming
Q22.hard
Which standard-library module is the toolbox for iterator tricks (count, cycle, chain, islice)?
itertools = lazy iterator building blocks: count(10) counts forever, cycle('AB') repeats, chain(a, b) concatenates lazily, islice takes slices of any iterator. Combine them like LEGO.
itertools
Q25.hard
What is the output?
g = (x for x in range(3))
print(2 in g)
print(list(g))
A True then [] - the membership test CONSUMED the generator up to (and past) 2
B True then [0, 1, 2]
C False then [0, 1, 2]
D Error
Answer: A β True then [] - the membership test CONSUMED the generator up to (and past) 2
'in' iterates the generator until it finds 2 - consuming 0, 1, 2 along the way. The generator is then exhausted (nothing after 2 in this case), so list(g) is []. Membership tests are destructive on iterators!
in on generatorsExhaustion traps
Q29.hard
You need to loop over data TWICE. Which is correct?
data = list(expensive_gen()) # materialize once
total = sum(data)
biggest = max(data) # second pass OK
A If it's a generator/iterator, materialize once: data = list(gen()) - then loop the list twice
B Loop the generator twice - it resets automatically
C Use reverse() to rewind it
D Iterators loop forever
Answer: A β If it's a generator/iterator, materialize once: data = list(gen()) - then loop the list twice
Iterators don't rewind. Either call the generator function again for a fresh one, or - if the data fits - convert to a list once and reuse it. Trying to re-loop an exhausted iterator silently gives zero iterations.