Intuition The 30-second picture
An iterator is a tape player with a single button: next . Press it, you get the next item; press it after the tape ends, it ==raises StopIteration==. A for loop is just code that keeps pressing that button until it hears the "tape ended" signal and quietly stops.
The deep idea: iteration is a protocol, not magic . Any object that obeys the protocol becomes loopable — even your own classes.
Intuition Why not just use a list?
A list stores every element in memory at once. If you want the first 10 prime numbers out of infinitely many, you can't store "all primes" in a list. An iterator computes one element at a time, on demand (lazy evaluation). This saves memory and lets you represent infinite or streaming sequences.
WHAT problem it solves: how can for x in something: work uniformly over lists, files, dictionaries, ranges, and your own objects, without for knowing their internals? Answer: they all agree to speak the same tiny iterator protocol .
Definition Iterable vs Iterator
An iterable is any object with an ==__iter__()== method that returns an iterator .
An iterator is any object with a ==__next__() method that returns the next item, and raises StopIteration== when exhausted. An iterator's __iter__() returns itself .
So: iterable = "I can give you a fresh tape player." Iterator = "I am the tape player; press my button."
Countdown iterator (derivation)
Goal: Countdown(3) should yield 3, 2, 1 then stop.
class Countdown :
def __init__ (self, start):
self .current = start # state lives on the object
def __iter__ (self):
return self # I am my own iterator
def __next__ (self):
if self .current <= 0 : # exhaustion check FIRST
raise StopIteration # signal: tape ended
value = self .current # remember what to return
self .current -= 1 # advance the state
return value # hand back this item
Why each step?
__iter__ returns self → satisfies "an iterator's __iter__ returns itself," so Countdown is both iterable and iterator.
The exhaustion check comes before computing/returning → otherwise we'd hand back 0 or negatives.
We save value before decrementing → we must return the current count, then prepare for next time.
Run:
for n in Countdown( 3 ):
print (n) # 3, 2, 1
infinite iterator: even numbers
class Evens :
def __init__ (self):
self .n = - 2
def __iter__ (self):
return self
def __next__ (self):
self .n += 2 # never raises StopIteration → infinite!
return self .n
Why this matters: A list of all even numbers is impossible; an iterator of them is trivial. Use with care — never list(Evens()) (it hangs forever). Use next() manually or itertools.islice.
Worked example Manual driving with
next() and a default
it = iter ([ 10 , 20 ])
print ( next (it)) # 10 — Why? first button press
print ( next (it)) # 20
print ( next (it, 'done' )) # 'done' — Why? default suppresses StopIteration
next(it, default) returns default instead of raising — handy when you want one item without a try/except.
Intuition Why split them apart?
A list is iterable but NOT its own iterator. Each iter(my_list) gives a brand-new iterator with its own position. That's why you can run two nested for loops over the same list independently:
nums = [ 1 , 2 ]
for a in nums: # iterator A
for b in nums: # iterator B — independent position
print (a, b) # (1,1)(1,2)(2,1)(2,2)
If a list were its own iterator (single shared position), the inner loop would exhaust it and the outer loop would never advance. This is the whole reason for the iterable/iterator distinction.
for loop on my class runs only once, then is empty forever."
Why the wrong idea feels right: You made __iter__ return self, which works — once. But because the object IS the iterator, its state (self.current) is consumed and never resets. Looping again starts from the exhausted state.
The fix: For reusable iterables, return a fresh iterator object from __iter__:
class CountdownReusable :
def __init__ (self, start): self .start = start
def __iter__ (self):
return Countdown( self .start) # new tape player each loop
return instead of raise StopIteration to stop."
Why it feels right: in a generator return ends iteration. But in a hand-written __next__, return just returns a value (often None) — the loop keeps going forever or yields Nones.
The fix: explicitly raise StopIteration in __next__.
__iter__ and only wrote __next__."
Why it feels right: __next__ looks like the core. But for/iter() call __iter__ first; without it you get TypeError: object is not iterable.
The fix: always provide both; the iterator's __iter__ returns self.
StopIteration inside a generator on purpose.
Since PEP 479, a StopIteration that bubbles out of a generator becomes a RuntimeError. In generators use return; only hand-written iterator classes raise StopIteration.
Recall The 20% that gives 80%
iter(obj) → calls obj.__iter__() → returns an iterator.
next(it) → calls it.__next__() → next item, or raises StopIteration.
for = iter + repeated next + catch StopIteration + break.
An iterator's __iter__ returns self; a reusable iterable returns a new iterator.
Recall Feynman: explain to a 12-year-old
Imagine a Pez candy dispenser. You press the top (next ) and one candy pops out. You keep pressing and eating. When it's empty, pressing makes a little click that means "all gone" (StopIteration ) — that's how your friend (the for loop ) knows to stop asking. The dispenser itself is the iterator . A bag of candy is like a list : it's not a dispenser, but you can always pull out a fresh dispenser from it whenever you want to share — that's what __iter__ does for a list: it hands you a new dispenser each time.
Mnemonic Remember the protocol
"I Need to Stop" →
I ter (__iter__ gives the iterator),
N ext (__next__ gives the item),
Stop Iteration (signals the end).
Generators — yield, lazy evaluation — generators auto-implement this protocol for you.
for loops and comprehensions — built on iter/next under the hood.
itertools — islice, count, chain — composable iterators, safe handling of infinite ones.
Lazy evaluation and memory efficiency — the WHY behind iterators.
Dunder (magic) methods — __iter__/__next__ are part of this family.
StopIteration and PEP 479 — why generators must return, not raise.
What two methods define the iterator protocol? __iter__ (returns the iterator) and __next__ (returns next item / raises StopIteration).
What does an iterator's __iter__ return? self.
What is the difference between an iterable and an iterator? An iterable has __iter__ returning an iterator; an iterator has __next__ and is consumed as you advance.
How does for x in obj work internally? it = iter(obj); loop next(it) in a try, catch StopIteration to break.
How does an iterator signal it is exhausted? It raises StopIteration.
What does next(it, default) do? Returns default instead of raising StopIteration when exhausted.
Why are lists not their own iterators? So each iter(list) gives an independent position, allowing nested/independent loops.
Why must __iter__ of a reusable iterable return a NEW iterator? Otherwise shared state gets exhausted and re-looping yields nothing.
In a hand-written __next__, how do you stop iteration? raise StopIteration (not return).
Inside a generator, how do you stop — and why not raise StopIteration? Use return; since PEP 479 a leaked StopIteration becomes a RuntimeError.
List stores all in memory
Need lazy on-demand values
Intuition Hinglish mein samjho
Dekho, iterator ka matlab hai ek aisa object jiske paas sirf ek button hai: next . Jab bhi tum button dabaate ho (next(it)), tumhe agla element milta hai. Jab list khatam ho jaati hai, toh wo StopIteration raise karta hai — yeh "tape khatam" ka signal hai, error nahi. Jo for loop hum daily likhte hain, wo internally yahi karta hai: pehle iter(obj) se tape player nikalta hai, phir baar baar next() dabaata hai, aur jaise hi StopIteration aaya, chup-chaap break kar deta hai.
Important baat — iterable aur iterator alag cheez hain. List ek iterable hai, par khud iterator nahi. Har baar iter(list) ek naya iterator deta hai apni alag position ke saath. Isi wajah se tum nested loops same list par chala sakte ho bina position gadbad hue. Agar tum apni class banaate ho, toh __iter__ aur __next__ dono likhne padte hain; iterator ka __iter__ self return karta hai.
Yeh kyun important hai? Kyunki iterator lazy hota hai — ek time pe ek element banata hai, poori list memory mein store nahi karta. Isiliye infinite sequences (jaise saare even numbers) ya badi files ko handle karna possible hota hai. Bas yaad rakhna: hand-written __next__ mein rukna ho toh return mat karo, raise StopIteration karo — warna loop kabhi khatam nahi hoga. Aur agar object ek hi baar loop ho raha hai dobara nahi, toh samajh jao ki uski state consume ho gayi — reusable banane ke liye __iter__ se naya iterator return karwao.