Iterators & Context Managers Deep Dive
🔄 Iterators & Context Managers Deep Dive
yield as an easy way to produce lazy sequences. This chapter goes underneath that: the iterator protocol every for loop actually relies on, how to implement it yourself in a class, and the same "deep dive" treatment for context managers — the real __enter__/__exit__ protocol behind with, plus contextlib's easier decorator-based shortcut.
🔁 The Iterator Protocol
Every for loop you've written since Course 1 is sugar for a lower-level protocol: calling iter() once, then next() repeatedly until StopIteration is raised:
An object is iterable if it defines __iter__ (which iter() calls, returning an iterator). An object is an iterator if it defines __next__ (which next() calls, returning the next value or raising StopIteration when exhausted). A list is iterable but is not itself an iterator — iter(numbers) returns a separate list_iterator object that actually tracks position.
🏗️ Building a Custom Iterator Class
This is exactly what Course 2, Chapter 5's count_up_to generator function did automatically — a generator function is really just a much more convenient way to get an object with __iter__ and __next__ already implemented correctly, without writing the class yourself.
Because Countdown.__iter__ returns self, and self.current only ever counts down, a single Countdown instance is exhausted after one full loop through it — iterating the same instance again yields nothing, the exact same one-pass limitation Course 2, Chapter 5 covered for generators. Create a fresh instance (Countdown(3) again) for a second pass, the same fix that chapter recommended for generators.
🔒 Context Managers: __enter__ and __exit__
This is the class-based version of Course 2, Chapter 4's with open(...) as file: — __enter__ runs at the start of the with block (its return value becomes the as variable), and __exit__ always runs at the end, even if an exception occurred inside the block.
__exit__ receives details about any exception that occurred inside the with block (exc_type, exc_value, traceback — all None if nothing went wrong). If __exit__ returns a truthy value, Python treats the exception as handled and suppresses it entirely — the code after the with block keeps running as if nothing happened. This is almost never what you want; return False (or nothing, which is falsy) unless you deliberately intend to swallow specific exceptions.
🎁 contextlib.contextmanager
@contextmanager turns a generator function into a context manager: code before yield becomes __enter__'s logic, code after yield (inside finally, guaranteeing it runs) becomes __exit__'s logic. For simple cases, this is noticeably less boilerplate than writing a full class with both dunder methods — the same trade-off decorators (Course 2, Chapter 6) generally offer over writing the underlying mechanism by hand.
Custom Iteration: Go vs Kotlin vs Python
| Language | Approach |
|---|---|
| Go | No built-in iterator protocol until relatively recently (range-over-func) — historically, channels combined with goroutines were the idiomatic way to produce a lazy sequence. |
| Kotlin | An Iterator<T> interface with hasNext() and next() — structurally very close to Python's __next__/StopIteration pair, just with an explicit boolean check instead of an exception. |
| Python | __iter__/__next__, with StopIteration as the "no more values" signal instead of a boolean check. |
Kotlin's explicit hasNext() check versus Python's exception-based StopIteration is a real philosophical difference worth noticing — Python leans on "ask forgiveness, not permission" here, the same style choice that shows up in its exception-handling culture generally (Course 2, Chapter 3).
__iter__ / __next__
The protocol every for loop relies on; next() raises StopIteration when done.
Iterable vs iterator
Iterable has __iter__; an iterator additionally has __next__.
__enter__ / __exit__
The class-based protocol behind with; __exit__ must return falsy to not suppress exceptions.
@contextmanager
Turns a yield-based generator function into a context manager, less boilerplate than a class.
💻 Coding Challenges
Challenge 1: An Evens-Only Iterator
Write a class EvensUpTo implementing __iter__ and __next__, that iterates only the even numbers from 0 up to (and including) a given limit. Use it in a for loop with a limit of 10.
Goal: Practice implementing the iterator protocol directly in a class, reusing this chapter's Countdown structure.
Challenge 2: A Class-Based Logging Context Manager
Write a class LogSection that takes a name in __init__, prints f"Entering {name}" in __enter__, and prints f"Exiting {name}" in __exit__. Use it in a with block around a couple of print statements.
Goal: Practice writing __enter__/__exit__ directly, reusing this chapter's Timer structure.
Challenge 3: The Same Logging Context Manager with contextlib
Rewrite Challenge 2's LogSection as a @contextmanager-decorated generator function called log_section(name) instead, producing the same "Entering .../Exiting ..." output.
Goal: Directly compare the class-based and @contextmanager-based approaches to the exact same problem.
🎯 What's Next
Next chapter: Concurrency — threading, multiprocessing, and the GIL explained.