Iterators, Generators & the Dunder Protocols
Python has no interfaces — a syntax works on your object when the object supplies the right dunder. Desugar a for-loop into iter/next/StopIteration, watch a generator freeze mid-frame on yield, and tour the protocol family behind in, len, bool, with, and ==. Computed and animated.
Concept · Languages & Runtimes. The source ↗
A free, interactive, animated visual explainer of Iterators, Generators & the Dunder Protocols — built to be understood, not skimmed.
Questions
- What is the difference between an iterable and an iterator in Python?
- An iterable is anything you can loop over — it supplies an __iter__() method (or an old-style __getitem__()) that hands back a fresh iterator. An iterator is the one-pass cursor: it has a __next__() method that returns the next item or raises StopIteration, and its own __iter__() returns itself. The practical consequence is reuse: a container "produces a fresh new iterator each time you pass it to the iter() function or use it in a for loop," whereas "attempting this with an iterator will just return the same exhausted iterator object used in the previous iteration pass, making it appear like an empty container."
- What does yield do in Python?
- A yield expression turns an ordinary function into a generator function: calling it runs no code, it just returns a generator (an iterator). "Each yield temporarily suspends processing, remembering the execution state (including local variables and pending try-statements). When the generator iterator resumes, it picks up where it left off (in contrast to functions which start fresh on every invocation)." So a generator is a function whose frame you can pause and resume, producing one value per yield lazily.
- What happens if you iterate a generator twice?
- The second loop sees nothing. A generator is a single-pass iterator, not a re-iterable container: the first loop drives it to StopIteration and the frame is gone. The data model requires that "once an iterator’s __next__() method raises StopIteration, it must continue to do so on subsequent calls," so the second for-loop gets StopIteration immediately and runs zero times. To iterate twice, either rebuild the generator (call the function again) or materialise it into a list.
- Why must __eq__ and __hash__ be defined together?
- Because hash-based collections rely on the invariant that objects which compare equal have the same hash. If you override __eq__ to compare by value but leave __hash__ alone, Python protects that invariant by making your objects unhashable: "if it defines __eq__() but not __hash__(), its instances will not be usable as items in hashable collections" — so they cannot be dict keys or set members. Define __hash__ to match your __eq__ (hashing the same fields), and keep those fields immutable.
- What is the difference between duck typing and typing.Protocol?
- Duck typing is the runtime style: "the method or attribute is simply called or used" without checking the type — if it has close(), you call close(). typing.Protocol makes that same structural expectation checkable by a static type checker: a function annotated with a Protocol accepts any object that structurally has the required methods, "recognized by static type checkers that recognize structural subtyping (static duck-typing)." Decorate the Protocol with @runtime_checkable and isinstance() works too, though it "will check only the presence of the required methods or attributes, not their type signatures."