Excellent.
DAY 23 is where Python’s scalability and elegance really show up. Iterators and generators are not conveniences — they are state machines + lazy execution.

After today, you’ll:

  • Think in streams, not lists
  • Avoid memory blowups
  • Understand how for actually works at runtime
  • Ace iterator/generator interview questions

🧠 DAY 23 — Iterators & Generators

(Lazy Evaluation, Protocols & State Machines)


🔑 CORE IDEA OF DAY 23

Iteration in Python is a protocol.
Generators are resumable functions that implement this protocol automatically.


1️⃣ The Iterator Protocol (The Contract)

An iterator must implement:

  • __iter__() → returns iterator object
  • __next__() → returns next value or raises StopIteration

That’s it.

class CountUp:
    def __init__(self, n):
        self.n = n
        self.i = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.i >= self.n:
            raise StopIteration
        self.i += 1
        return self.i

Now:

for x in CountUp(3):
    print(x)

2️⃣ How for REALLY Works (Recap with Precision)

for x in iterable:
    body

Is equivalent to:

it = iter(iterable)
while True:
    try:
        x = next(it)
    except StopIteration:
        break
    body

🧠 Every for loop is try/except-driven.


3️⃣ Iterables vs Iterators (INTERVIEW FAVORITE)

TermMeaning
IterableHas __iter__()
IteratorHas __iter__() + __next__()

Examples:

iter([1, 2, 3])   # iterator
[1, 2, 3]         # iterable

Key rule:

An iterator is exhaustible.
An iterable can create new iterators.


4️⃣ Why Lists Are NOT Iterators

lst = [1, 2, 3]
it = iter(lst)

list(it)   # [1, 2, 3]
list(it)   # []   (exhausted)

But:

list(lst)  # [1, 2, 3] every time

Because:

  • lst → iterable
  • iter(lst) → iterator

5️⃣ Generators — The Shortcut to Iterators

A generator function:

  • Contains yield
  • Returns a generator object
  • Automatically implements iterator protocol
def count_up(n):
    i = 0
    while i < n:
        i += 1
        yield i

Usage:

for x in count_up(3):
    print(x)

No class needed.
No __next__ written manually.


6️⃣ What yield REALLY Does (Critical Insight)

yield:

  • Produces a value
  • Pauses function execution
  • Saves local state
  • Resumes on next next()

This turns a function into a state machine.


7️⃣ Generator State Machine (Visual)

Image
Image
Image
Image

States:

CREATED → RUNNING → SUSPENDED → RUNNING → DONE

Once DONE → StopIteration forever.


8️⃣ Generator Memory Advantage (WHY THEY MATTER)

Compare:

lst = [x*x for x in range(10_000_000)]

vs

gen = (x*x for x in range(10_000_000))
  • List → allocates all memory upfront
  • Generator → computes on demand

🧠 This is why generators are essential for:

  • Big data
  • Streaming
  • Pipelines
  • ETL logic

9️⃣ Generator Expressions vs List Comprehensions

(x*x for x in range(5))   # generator
[x*x for x in range(5)]  # list

Differences:

  • Generator is lazy
  • List is eager
  • Generator uses constant memory

Rule:

Use generator unless you need random access or reuse.


🔟 yield from (Advanced but Powerful)

def chain(a, b):
    yield from a
    yield from b

Equivalent to:

for x in a:
    yield x
for x in b:
    yield x

Benefits:

  • Cleaner code
  • Faster delegation
  • Proper exception forwarding

11️⃣ Generator Exhaustion Trap 🔥

g = (x for x in range(3))
print(list(g))
print(list(g))

Output:

[0, 1, 2]
[]

Once consumed, it’s gone.


12️⃣ Exceptions Inside Generators

def gen():
    yield 1
    raise ValueError("boom")
    yield 2
for x in gen():
    print(x)
  • Exception propagates normally
  • Generator terminates

Generators are exception-aware.


🔥 INTERVIEW TRAPS (DAY 23)

Q1

g = (i for i in range(3))
print(next(g))
print(list(g))

✔ Output:

0
[1, 2]

Q2

Why generators are faster for pipelines?

✔ No intermediate lists
✔ Lazy evaluation
✔ Less memory pressure


Q3

Can a generator be restarted?

❌ No
✔ Create a new generator instead


🧠 DAY 23 MENTAL CHECKLIST

When handling large data:

  1. Can this be streamed?
  2. Do I really need a list?
  3. Is one-pass enough?
  4. Will exhaustion cause bugs?
  5. Should I document generator behavior?

📝 DAY 23 ASSIGNMENT (MANDATORY)

1️⃣ Predict output (NO RUNNING):

def g():
    yield 1
    yield 2

x = g()
print(next(x))
print(list(x))
print(list(x))

2️⃣ Explain clearly:

  • Difference between iterable and iterator
  • How yield preserves state

3️⃣ Design question:

How would you design a memory-efficient log processing pipeline in Python?

(Hint: generators + composition)


🔜 DAY 24 PREVIEW

DAY 24 — Comprehensions & Lazy Evaluation (Performance & Readability Tradeoffs)

You’ll learn:

  • List vs set vs dict comprehensions
  • Nested comprehensions (and when NOT to use them)
  • Generator pipelines
  • Readability vs cleverness

When ready, say 👉 “START DAY 24”