πŸŽ“ Velnov

🐍 Python · Topic 17 of 20

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

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
Output1
[2, 3]

Pause and resume

def demo():
    print("start")
    yield 1
    print("resumed")
    yield 2

g = demo()
print("before")
print(next(g))
print(next(g))
Outputbefore
start
1
resumed
2

3. Why generators save memory

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

MisconceptionCorrect 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

  1. What happens when next() is called on a used-up iterator?

    Show answer

    StopIteration is raised (unless a default is given).

  2. What makes a function a generator function?

    Show answer

    It contains yield.

  3. What is the key difference between yield and return?

    Show answer

    yield pauses and resumes; return ends the function.

  4. Why does a generator save memory?

    Show answer

    It produces values one at a time instead of storing them all.

  5. What does list(g) give the second time for the same generator?

    Show answer

    [] (it is already used up).

  6. Complete the Fibonacci step after yield a.

    Show answer

    a, b = b, a + b

  7. How do you take the first 5 values of an endless generator?

    Show answer

    itertools.islice(gen(), 5)

  8. 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.

Key concepts in this topic

iter()next()StopIterationnext() defaultIterator protocolfor loopsyieldGeneratorsFunction stateGenerator objectsLazy evaluationMemory efficiencyStreamingInfinite generatorsitertools.isliceOne-pass iterationExhaustion__next__rangeLazy sequencesmap()filter()Python 2 vs 3sum()

Jump to: Easy Β· Medium Β· Hard

Easy questions (3)

Q4. easy

What makes a function a GENERATOR function?

def count_up(n):
    for i in range(1, n + 1):
        yield i

g = count_up(3)
print(list(g))   # [1, 2, 3]
  1. A It contains the yield keyword
  2. B It's named with gen_
  3. C It returns a list
  4. 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]?

  1. A (x*2 for x in nums)
  2. B {x*2 for x in nums}
  3. C lazy[x*2 for x in nums]
  4. 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:

  1. A A list_iterator object ready for next() calls
  2. B The same list
  3. C The first element
  4. 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?

nums = [1, 2, 3]        # iterable
it = iter(nums)          # iterator
print(next(it))          # 1
print(next(it))          # 2
  1. A An iterable can PRODUCE an iterator (lists, strings); an iterator is the object that actually yields values one by one via next()
  2. B They are exact synonyms
  3. C Iterators are always faster
  4. 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
  1. A Raises StopIteration
  2. B Returns None forever
  3. C Restarts from the beginning
  4. 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
  1. A return exits for good; yield PAUSES the function, remembers everything, and resumes on the next next()
  2. B yield is just a faster return
  3. C return sends multiple values
  4. 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())

print(gen())         # <generator object ...>
print(list(gen()))   # ['a', 'b']
  1. A <generator object gen at 0x...> - calling it produces a generator, not values
  2. B a
  3. C ['a', 'b']
  4. 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
  1. A It produces one value at a time on demand - never storing the whole sequence
  2. B It compresses the list
  3. C It uses the GPU
  4. 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?

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)))   # [3, 2, 1]
  1. A __iter__ (returns self) and __next__ (returns the next value or raises StopIteration)
  2. B __loop__ and __stop__
  3. C __for__ and __in__
  4. 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
  1. A range is lazy - it stores only start/stop/step and computes values on demand
  2. B Python compresses the numbers
  3. C It secretly fails
  4. 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.

rangeLazy sequences

Q12. medium

In Python 3, what do map() and filter() return?

m = map(str.upper, ["a", "b"])
print(m)          # <map object ...>
print(list(m))    # ['A', 'B']
print(list(m))    # [] - exhausted!
  1. A Lazy iterator objects - wrap in list() to see all values
  2. B Lists, like Python 2
  3. C Tuples
  4. 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)))

  1. A 14
  2. B 30
  3. C 6
  4. 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
  1. A A generator expression can drop its parentheses when it's the ONLY argument of a call
  2. B It's actually a list comprehension
  3. C sum() has special syntax
  4. 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!
  1. A next(g)
  2. B g[0]
  3. C g.first()
  4. 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)

  1. A C then A - the body doesn't start until the first next()
  2. B A then C
  3. C A B C
  4. 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")
  1. A Returns "default" instead of raising StopIteration when the iterator is exhausted
  2. B Sets the iterator's default forever
  3. C Skips one value
  4. 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
  1. A Iterable only - iter("abc") produces the iterator; the string itself has no __next__
  2. B Iterator only
  3. C Both at once
  4. 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?

  1. 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)
  2. B Files load fully into RAM on open
  3. C You can index f[3]
  4. 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}"}
  1. A Consumers can process users one at a time without a million-object list in memory - and stop early for free
  2. B Generators create more realistic data
  3. C Lists can't hold a million items
  4. 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()))

  1. A [1] - return stops the generator; the second yield is never reached
  2. B [1, 2]
  3. C [1, None, 2]
  4. 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?

  1. A Large/unbounded data, streaming pipelines, or when callers may stop early - laziness pays off
  2. B Always - lists are obsolete
  3. C Never - lists are always simpler
  4. 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
  1. A Calls iter(nums) once, then next() repeatedly until StopIteration
  2. B Indexes nums[0], nums[1]... until IndexError
  3. C Copies the list first
  4. 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

g = naturals()
print(next(g), next(g), next(g))   # 1 2 3

from itertools import islice
print(list(islice(naturals(), 5)))  # [1, 2, 3, 4, 5]
  1. A Yes - it only computes values as requested, so the infinite loop is harmless
  2. B No - it would hang immediately
  3. C Only up to 1 million
  4. 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
  1. A [1, 2] then [] - generators are ONE-PASS; once exhausted, they're empty forever
  2. B [1, 2] then [1, 2]
  3. C [1, 2] then Error
  4. 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]
  1. A a, b = b, a + b
  2. B a = b + a
  3. C b = a
  4. 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
  1. A Delegates to another iterable - yielding each of ITS values in turn: 1, 2, 3, 4
  2. B Imports yields from another file
  3. C Yields the list objects themselves
  4. 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)

  1. A Chained lazy stages - each line flows through filter and slice one at a time; nothing processes until you iterate the last stage
  2. B Running generators in parallel threads
  3. C A pipe character syntax
  4. 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)?

from itertools import count, islice, chain
print(list(islice(count(10), 3)))     # [10, 11, 12]
print(list(chain([1, 2], [3])))        # [1, 2, 3]
  1. A itertools
  2. B itermod
  3. C looptools
  4. D generators

Answer: A β€” itertools

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))

  1. A True then [] - the membership test CONSUMED the generator up to (and past) 2
  2. B True then [0, 1, 2]
  3. C False then [0, 1, 2]
  4. 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
  1. A If it's a generator/iterator, materialize once: data = list(gen()) - then loop the list twice
  2. B Loop the generator twice - it resets automatically
  3. C Use reverse() to rewind it
  4. 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.

ExhaustionMaterializing

Practice β€œIterators & Generators” as a quiz