Skip to contentSkip to navigation
MODULE 10 · LESSON 01

The Iterator Protocol

Implement __iter__ and __next__, handle StopIteration signals, and make custom objects iterable.

Core Learning Milestones:
Iterable vs Iterator
__iter__ vs __next__
StopIteration
The forward-only signal
Safe next() Defaults
next(it, default)
Sentinel & enumerate()
iter(func, sentinel)
Estimated Time
30 Minutes
Comprehensive deep dive
Difficulty Level
Intermediate
Builds on Dunder Methods
Knowledge Check
15 Questions
+35 XP Potential
01 / Foundations

Loops Don't Use Indices — They Use the Iterator Protocol

A for loop never touches raw indices under the hood. It calls iter(obj) once to get an iterator, then calls next(iterator) repeatedly until it catches a StopIteration exception.

Every iterator is an iterable, but not every iterable is an iterator — a list is iterable, but calling iter(my_list) produces a brand new list-iterator object holding a pointer to the current index.

Manual index counters
for i in range(len(items)):
print(i, items[i])
Idiomatic enumerate()
for i, item in enumerate(items):
print(i, item)
Think of it like this:

An iterator is like a bookmark in a book: it knows exactly which page is next, and advancing it gives you one sentence at a time. Once you've turned past a page, there's no going back — iterators are forward-only.

Full Contract in Action

Custom Stateful Iterator Implementation

Run the snippet below to see a custom class implement the full iterator contract:

iterator_protocol.py — Press 'Run Code' to execute in your browser
class StepRange:
    def __init__(self, start, stop, step=1):
        self.current = start
        self.stop = stop
        self.step = step

    def __iter__(self):
        # Return self: an iterator must return itself from __iter__
        return self

    def __next__(self):
        if self.current >= self.stop:
            raise StopIteration
        val = self.current
        self.current += self.step
        return val

# Manual consumption with next()
stepper = StepRange(0, 10, 3)
print("First:", next(stepper))
print("Second:", next(stepper))

# Consuming the remainder in a for-loop
remaining = [n for n in stepper]
print("Remaining elements:", remaining)
Iterator Protocol Glossary: Core Concepts
__iter__Iterable

Returns a fresh iterator object — required by every collection that supports for-loops.

__next__Iterator

Advances state and returns the next value, or raises StopIteration when exhausted.

StopIterationSignal

The exception a for-loop silently catches to know an iterator has been fully consumed.

enumerate()Helper

Lazily yields (index, item) pairs, eliminating manual loop counters entirely.

02 / Protocol Dispatch Tracer

Trace Exactly What Happens Inside a for-loop

Click through each phase of iterator dispatch, from acquisition to exhaustion:

iter(obj) -> __iter__
stepper = StepRange(0, 10, 3) # for-loop calls iter(stepper) first # __iter__ returns self

Any iterable exposes __iter__(), which returns an iterator object — often itself if the class is also the iterator.

03 / Iterable vs Iterator Reference

Contracts, State, and Built-in Helpers

These three families cover nearly every iteration pattern you'll write in application code.

Iterable contract

__iter__(self) -> iterator

Returns a fresh iterator object every time it's called — list, dict, str, set, and range all implement this.

Iterator contract

__next__(self) -> item

Maintains state, returns the next item, and raises StopIteration when exhausted.

__iter__(self) -> self

An iterator must return itself here, so it can be used anywhere an iterable is expected.

Built-in helpers

next(it, default)

Safe consumption pattern — returns default instead of raising StopIteration when exhausted.

iter(callable, sentinel)

Two-argument sentinel form: calls callable() repeatedly until it returns sentinel.

enumerate(seq, start=N)

Lazily yields (index, item) pairs without a manual counter variable.

04 / Hands-on Examples

Worked Hands-on Walkthroughs

Progress step-by-step from a custom iterator through sentinel-driven streams to safe defensive consumption:

Walkthrough 01 · FoundationLevel 1 · Foundation

Custom Stateful Iterator Implementation

Custom Iterator
Problem Scenario

The iterator maintains internal state across calls. Once consumed, subsequent calls trigger StopIteration.

Executable Solution
Custom Stateful Iterator Implementation
class StepRange:
    def __init__(self, start, stop, step=1):
        self.current = start
        self.stop = stop
        self.step = step

    def __iter__(self):
        # Return self: an iterator must return itself from __iter__
        return self

    def __next__(self):
        if self.current >= self.stop:
            raise StopIteration
        val = self.current
        self.current += self.step
        return val

# Manual consumption with next()
stepper = StepRange(0, 10, 3)
print("First:", next(stepper))
print("Second:", next(stepper))

# Consuming the remainder in a for-loop
remaining = [n for n in stepper]
print("Remaining elements:", remaining)
Step-by-Step Explanation

The iterator maintains internal state across calls. Once consumed, subsequent calls trigger StopIteration.

Walkthrough 02 · Building UpLevel 2 · Applied

Sentinel Iteration over IO Streams and Callables

Sentinel Form
Problem Scenario

The sentinel form cleanly consumed stream lines until encountering 'EOF\n', ignoring trailing data.

Executable Solution
Sentinel Iteration over IO Streams and Callables
import io

# Simulating incoming socket data stream with an end marker
raw_data = "CHUNK_1\nCHUNK_2\nCHUNK_3\nEOF\nIGNORED_DATA"
stream = io.StringIO(raw_data)

# iter(stream.readline, 'EOF\n') stops automatically at the sentinel!
chunk_iterator = iter(stream.readline, "EOF\n")

chunks = [line.strip() for line in chunk_iterator]
print("Processed chunks:", chunks)
Step-by-Step Explanation

The sentinel form cleanly consumed stream lines until encountering 'EOF\n', ignoring trailing data.

Walkthrough 03 · MasteryLevel 3 · Architecture

Custom Start Offsets with enumerate()

Safe Defaults
Problem Scenario

enumerate cleanly generates indices on the fly without holding index variables outside loop scope.

Executable Solution
Custom Start Offsets with enumerate()
tasks = ["Deploy migrations", "Run security scan", "Warm CDN cache"]

# 1-based display numbering
print("Deployment Steps:")
for step_num, task in enumerate(tasks, start=1):
    print(f"  Step {step_num}: {task}")

# Building an indexed lookup table
task_index = {task: idx for idx, task in enumerate(tasks, start=100)}
print("Task ID Map:", task_index)
Step-by-Step Explanation

enumerate cleanly generates indices on the fly without holding index variables outside loop scope.

05 / Traps to Avoid

Common Traps & How to Avoid Them

Study the mistakes and clean fixes below before writing your own iterators:

!Trap 1: Syntax or Name Typos in The Iterator Protocol
Common Trap
The Bug (Avoid)Buggy Code
# Mistyped variable or missing closing parenthesis result = the_iterator_protocol_val
What Happens:
NameError: name is not defined
The Clean FixCorrected Code
# Declare and verify variables before execution val = 10 print("Defined correctly:", val)
Recommended Practice:
Prefer next(it, default) and enumerate() over manual bookkeeping around exhausted iterators.
Why It Happens

Beginners frequently encounter typos or forgotten definitions before using an identifier.

!Trap 2: Unintended State Mutation or Scope Leakage
Subtle Trap
The Bug (Avoid)Buggy Code
# Modifying shared state or unexpected variable shadowing def update_state(data=[]): data.append(1) return data
What Happens:
Default mutable argument accumulates values across separate function calls.
The Clean FixCorrected Code
def update_state(data=None): if data is None: data = [] data.append(1) return data
Recommended Practice:
Prefer next(it, default) and enumerate() over manual bookkeeping around exhausted iterators.
Why It Happens

Default arguments in Python are evaluated once at function definition time, not at invocation time.

!Trap 3: Resource Leaks & High-Scale Performance Regressions
Architecture Trap
The Bug (Avoid)Buggy Code
# Opening resources in a tight loop without closing or bounding memory files = [open(f"log_{i}.txt", "w") for i in range(5000)]
What Happens:
OSError: [Errno 24] Too many open files (file descriptor exhaustion)
The Clean FixCorrected Code
for i in range(5): with open(f"log_{i}.txt", "w") as f: f.write("Safe")
Recommended Practice:
Prefer next(it, default) and enumerate() over manual bookkeeping around exhausted iterators.
Why It Happens

Failing to release system resources leads to process termination under heavy production workloads.

06 / Real-World Systems

Real-World Systems in Action

See how scripts, streaming services, and high-throughput pipelines all lean on the iterator protocol:

Developer ProductivityFoundation Script

Local Developer Automation Tool

SYSTEM #1
Organization Context

Individual developers and IT administrators automate everyday repetitive workflows.

Why This Pattern Works Here

A clean standalone script providing fast, local automation with zero dependencies.

Production Code Wrapper
Local Developer Automation Tool
items = [10, 20, 30]
iterator = iter(items)
print("First:", next(iterator))
print("Second:", next(iterator))
Cloud MicroservicesScaled Service · Applied

Backend API Service Integration

SYSTEM #2
Organization Context

SaaS platforms run backend microservices processing requests concurrently.

Why This Pattern Works Here

A modular service handler integrating validation and structured responses.

Production Code Wrapper
Backend API Service Integration
# Service integration layer
def handle_request(payload: dict) -> dict:
    return {"status": "success", "processed": True}
Distributed SystemsEnterprise · Architecture

High-Throughput Cloud Architecture

SYSTEM #3
Organization Context

Database cursor streams fetch millions of records in bounded batches using iterators to prevent out-of-memory container crashes.

Why This Pattern Works Here

Database cursor streams fetch millions of records in bounded batches using iterators to prevent out-of-memory container crashes.

Production Code Wrapper
High-Throughput Cloud Architecture
# High-resilience production pattern
import time

def process_stream(events: list) -> int:
    processed = 0
    for event in events:
        if event:
            processed += 1
    return processed
Key Takeaways

Key Points & Concept Review

  • Iterables provide an __iter__() method; iterators provide both __iter__() and __next__().
  • next(it, default) provides safe, exception-free retrieval on exhausted iterators.
  • The sentinel form iter(func, sentinel) repeatedly invokes func() until it produces sentinel.
  • enumerate(seq, start=N) yields (index, item) pairs lazily without materializing index arrays.
  • Iterators are forward-only state machines that cannot be rewound once exhausted.
  • Lazy iteration enables processing massive datasets within fixed O(1) memory limits.
Study Guide & References

Frequently Asked Questions & Reference Hub

FAQ & References
Growth Mindset: Iterators Click Once You Trace One By Hand

Manually calling next() a few times on your own custom iterator is the single fastest way to internalize the protocol — once you've watched StopIteration fire yourself, every for-loop in Python becomes transparent.

Related Built-in References Right Here
PyLabs Library Docs
Ready for Action

You've Mastered the Iterator Protocol!

You understand iterables vs iterators, safe consumption, and sentinel-driven streams. Now lock in your knowledge with hands-on practice.

Hands-on Code Challenge

Implement a custom iterator with __iter__ and __next__.

+40 XP Reward

Earn experience points toward your developer rank as soon as you pass.

~3 Minutes to Complete

Bite-sized knowledge check with instant explanations for every question.

Worked example

Hand out daily pocket money one day at a time with a generator — values appear lazily, none stored up front.

Example
def pocket_money(amount, days):
    for _ in range(days):
        yield amount

wallet = pocket_money(20, 3)
print(next(wallet), next(wallet))

How it works, step by step

  1. yield pauses the function and hands out one value.
  2. next() resumes until the next yield.
  3. Nothing beyond the requested days is ever computed.

Your notes

Sign in to keep private notes alongside this lesson.

Sign in to take notes

The Iterator Protocol practice

Sign-in required

Write a generator function named count_up that yields 1, 2, and 3. Loop over it and print each value on its own line.

Coding Practice is Locked

Sign in to access the interactive Python editor, write and run code checks against automated tests, use hints, and earn XP.

Practice quiz

Check yourself

15 questions with explanations after submission, so you can correct course as you go.

The Iterator Protocol quiz

Locked

You must be signed in to take this quiz. Sign in to test your understanding, track your answers, and earn XP.

Lesson incomplete

Not completed

Create an ID or sign in to save lesson completion across devices.

Create ID to save
Module lesson progress0/5 complete
Module cheat sheet