Every Python for loop you have ever written relies on a two-step protocol that most developers never see because Python hides it behind clean syntax.
Understanding the protocol turns for loops from black boxes into predictable, traceable mechanisms.
What Python Does With Every For Loop
for item in collection:
process(item)
Enter fullscreen mode Exit fullscreen mode
Python expands this to:
iterator = iter(collection)
while True:
try:
item = next(iterator)
process(item)
except StopIteration:
break
Enter fullscreen mode Exit fullscreen mode
iter() calls collection.__iter__() and returns an iterator object.
next() calls iterator.__next__() and returns the next value.
When the iterator is exhausted, __next__ raises StopIteration and the loop ends.
This is the iteration protocol. Any object that implements __iter__ and __next__ can be iterated.
Why This Matters for Tracing
a = [1, 2, 3]
it = iter(a)
print(next(it)) # 1
print(next(it)) # 2
for item in it:
print(item)
Enter fullscreen mode Exit fullscreen mode
Output:
1
2
3
Enter fullscreen mode Exit fullscreen mode
The iterator is stateful. After calling next(it) twice manually, the iterator has consumed the first two values. The for loop continues from where the iterator left off and only sees 3.
This is why a generator, once partially consumed, resumes from its current position when you iterate it again.
enumerate: What It Actually Does
colors = ["red", "green", "blue"]
for i, color in enumerate(colors, start=1):
print(i, color)
Enter fullscreen mode Exit fullscreen mode
Output:
1 red
2 green
3 blue
Enter fullscreen mode Exit fullscreen mode
enumerate(colors, start=1) creates an enumerate object that wraps the original iterator. Each call to next() on the enumerate object calls next() on the inner iterator and returns a tuple of (current_count, value).
The tuple (i, color) in the for loop uses tuple unpacking to assign both values simultaneously.
zip: Combining Two Iterators
names = ["Alice", "Bob", "Carol"]
scores = [85, 92, 78, 95]
for name, score in zip(names, scores):
print(f"{name}: {score}")
Enter fullscreen mode Exit fullscreen mode
Output:
Alice: 85
Bob: 92
Carol: 78
Enter fullscreen mode Exit fullscreen mode
zip stops when the shortest iterator is exhausted. Carol’s 78 is included but the fourth score 95 is dropped because there is no fourth name.
zip_longest from itertools fills with a default value instead of stopping.
The Interview Problem: Custom Iterator
class CountDown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
for n in CountDown(3):
print(n)
Enter fullscreen mode Exit fullscreen mode
Trace the execution:
iter(CountDown(3)) returns the object itself because __iter__ returns self.
First next(): current is 3, returns 3, current becomes 2.
Second next(): current is 2, returns 2, current becomes 1.
Third next(): current is 1, returns 1, current becomes 0.
Fourth next(): current is 0, raises StopIteration.
Output:
3
2
1
Enter fullscreen mode Exit fullscreen mode
The Exhaustion Problem
numbers = [1, 2, 3, 4, 5]
evens = filter(lambda x: x % 2 == 0, numbers)
print(list(evens)) # [2, 4]
print(list(evens)) # []
Enter fullscreen mode Exit fullscreen mode
filter() returns a lazy iterator, not a list. After the first list(evens) call consumes all its values, the iterator is exhausted. The second call finds nothing remaining.
This surprises developers who treat filter, map, and zip objects as reusable sequences. They are single-pass iterators.
Practice iteration protocol tracing at pycodeit.com.
답글 남기기