Writing a Custom Iterator Class in Python With __iter__ and __next__
A for loop calls iter() once and next() until StopIteration. Write __iter__ and __next__ yourself, avoid the iterator that only works once, and know when a generator in __iter__ does the same job in fewer lines.
Write an iterator class when a for loop over your object should do something a list can't: produce values on demand, read from somewhere as it goes, or keep state between steps. Most of the time a generator function does the same job in fewer lines, and I'd use one first. But the class shows exactly what a for loop does, and some objects really do need it.
Skip ahead: If you only need the template, go to "The split that matters: iterable vs iterator". Single-class iterators have a bug that shows up the second time you loop.
What a for loop actually calls
items = ["coffee", "bread"]
it = iter(items)
print(next(it))
print(next(it))
try:
next(it)
except StopIteration:
print("StopIteration: the loop ends here")
coffee
bread
StopIteration: the loop ends here
for item in items: does exactly this. It calls iter(items) once to get an iterator, calls next() on it until StopIteration is raised, and stops quietly. To make your own object work in a for loop, you provide those two methods:
- **
__iter__** returns the iterator.iter()calls it. - **
__next__** returns the next value, or raisesStopIterationwhen there are none.next()calls it.
A first iterator: business days
from datetime import date, timedelta
class BusinessDays:
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
while self.current <= self.end:
day = self.current
self.current += timedelta(days=1)
if day.weekday() < 5:
return day
raise StopIteration
for day in BusinessDays(date(2026, 10, 9), date(2026, 10, 14)):
print(day, day.strftime("%a"))
2026-10-09 Fri
2026-10-12 Mon
2026-10-13 Tue
2026-10-14 Wed
It skips the weekend and never builds a list. A range of ten years costs the same memory as a range of a week, because only current is stored.
The bug: it only works once
from datetime import date, timedelta
class BusinessDays:
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
while self.current <= self.end:
day = self.current
self.current += timedelta(days=1)
if day.weekday() < 5:
return day
raise StopIteration
week = BusinessDays(date(2026, 10, 12), date(2026, 10, 16))
print("first loop:", len(list(week)))
print("second loop:", len(list(week)))
first loop: 5
second loop: 0
No error. The second loop is just empty. __iter__ returns self, and self.current is already past the end. That's correct for an iterator, which is used up as you go, just like a file you've read to the end. It's wrong for something people expect to loop over twice, like a date range in a report that counts the days and then prints them.
The split that matters: iterable vs iterator
The fix is two classes. The iterable holds the settings and makes a fresh iterator each time someone loops. The iterator holds the position:
from datetime import date, timedelta
class BusinessDays:
"""Iterable: loop over it as many times as you like."""
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
return _BusinessDayIterator(self.start, self.end)
class _BusinessDayIterator:
"""Iterator: used up after one pass."""
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
while self.current <= self.end:
day = self.current
self.current += timedelta(days=1)
if day.weekday() < 5:
return day
raise StopIteration
week = BusinessDays(date(2026, 10, 12), date(2026, 10, 16))
print("first loop:", len(list(week)))
print("second loop:", len(list(week)))
a = iter(week)
b = iter(week)
print(next(a), next(a), next(b))
first loop: 5
second loop: 5
2026-10-12 2026-10-13 2026-10-12
Two loops, two results, and two iterators that don't disturb each other. This is how lists, dictionaries and range work: range(5) is an iterable you can loop over again and again, and each iter(range(5)) is a new iterator.
⚠ Warning: An iterator's __iter__ must return self. Without it, code like for x in iter(obj) or zip() fails with TypeError: '...' object is not iterable, because Python's rule is that every iterator is also iterable.
The shorter way: make __iter__ a generator
Here's the same iterable with no second class:
from datetime import date, timedelta
class BusinessDays:
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
day = self.start
while day <= self.end:
if day.weekday() < 5:
yield day
day += timedelta(days=1)
week = BusinessDays(date(2026, 10, 12), date(2026, 10, 16))
print(len(list(week)), len(list(week)))
print([d.day for d in week])
5 5
[12, 13, 14, 15, 16]
Because __iter__ contains yield, calling it returns a brand-new generator every time, and a generator is an iterator. You get the two-class behavior with one class and no StopIteration. For most real code, this is the version I'd write.
So when do you still write __next__ by hand?
- You need extra methods on the iterator itself, like
peek()to see the next value without using it, or apositionyou can read mid-loop. - You're wrapping something with its own position, like a cursor from a database driver or a network stream, and you want the class to show clearly that it's one pass only.
- You want to understand what generators do for you. That's a fair reason too.
An iterator with peek()
class Peekable:
_empty = object()
def __init__(self, iterable):
self._it = iter(iterable)
self._next = self._empty
def __iter__(self):
return self
def __next__(self):
if self._next is not self._empty:
value, self._next = self._next, self._empty
return value
return next(self._it)
def peek(self, default=None):
if self._next is self._empty:
try:
self._next = next(self._it)
except StopIteration:
return default
return self._next
lines = Peekable(["# header", "a", "b"])
if lines.peek().startswith("#"):
print("skip:", next(lines))
print(list(lines))
print(lines.peek("nothing left"))
skip: # header
['a', 'b']
nothing left
next(self._it) raises StopIteration on its own when the source runs out, so __next__ doesn't need to raise it itself. _empty is a unique marker, so None can still be a real value in the data.
Common mistakes
- **
__iter__returningselfon something people loop over twice.** Second loop is silently empty. - **Returning
Noneinstead of raisingStopIteration.** The loop never ends, or yieldsNoneforever. - **Raising
StopIterationinside a generator.** In a generator, justreturn. AStopIterationraised inside one becomes aRuntimeError. - A full iterator class where a generator would do. More code, same result.
What happens if you get iterable vs iterator wrong
The report counts 5 business days and then prints none of them. Or a test passes alone and fails when another test looped over the same object first. Nothing crashes, so you look everywhere except the __iter__ method.
If you pass your object to a function that calls len(list(x)) and then loops over x, does your class survive that?
About
The iterator protocol, a business-days iterator, the iterable vs iterator split, __iter__ as a generator, and a peekable iterator. After this, your objects work in for loops, and the second loop isn't silently empty.
- Responsible
- Jay P
- Last Update
- 09/13/2026
- Reading Time
- 6 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.