The Complete Guide to Python's for Loop Internals

작성자

카테고리:

← 피드로
DEV Community · Ameer Abdullah · 2026-07-24 개발(SW)
Cover image for The Complete Guide to Python's for Loop Internals

Ameer Abdullah

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.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다