5.1. Iterator
Iterate over a collection of objects
Usecase: Iterate over a group of users
Usecase: Iterate over browser history
The Iterator pattern is a design pattern that provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. In Python, this is typically implemented using the __iter__ and __next__ methods.
5.1.1. Problem
>>> class Group:
... def __init__(self):
... self.users = []
...
... def add_user(self, user):
... self.users.append(user)
...
... def get_users(self):
... return self.users
>>>
>>>
>>> admins = Group()
>>> admins.add_user('alice')
>>> admins.add_user('bob')
>>> admins.add_user('carol')
>>>
>>> for i in range(len(admins.get_users())):
... print(admins.get_users()[i])
alice
bob
carol
5.1.2. Solution: Custom Iterator
>>> class Group:
... def __init__(self):
... self.users = []
...
... def add_user(self, user):
... self.users.append(user)
...
... def __iter__(self):
... self.current = 0
... return self
...
... def __next__(self):
... if self.current >= len(self.users):
... raise StopIteration
... user = self.users[self.current]
... self.current += 1
... return user
>>>
>>>
>>> admins = Group()
>>> admins.add_user('alice')
>>> admins.add_user('bob')
>>> admins.add_user('carol')
>>>
>>> for user in admins:
... print(user)
alice
bob
carol
5.1.3. Solution: Builtin Iterator
In Python you can access attributes directly
>>> class Group:
... def __init__(self):
... self.users = []
...
... def add_user(self, user):
... self.users.append(user)
>>>
>>>
>>> admins = Group()
>>> admins.add_user('alice')
>>> admins.add_user('bob')
>>> admins.add_user('carol')
>>>
>>> for user in admins.users:
... print(user)
alice
bob
carol
5.1.4. Rationale
>>> class Group:
... def __init__(self):
... self.users = []
...
... def add_user(self, user):
... self.users.append(user)
...
... def __iter__(self):
... self.current = 0
... return self
...
... def __next__(self):
... if self.current >= len(self.users):
... raise StopIteration
... user = self.users[self.current]
... self.current += 1
... return user
>>>
>>>
>>> admins = Group()
>>> admins.add_user('alice')
>>> admins.add_user('bob')
>>> admins.add_user('carol')
>>>
>>> hasattr(admins, '__iter__')
True
>>> hasattr(admins, '__next__')
True
>>>
>>> result = iter(admins)
>>> next(result)
'alice'
>>> next(result)
'bob'
>>> next(result)
'carol'
>>> next(result)
Traceback (most recent call last):
StopIteration
5.1.5. Internals
Iterable:
__iter__
Iterator:
__iter__
,__next__
Generator:
__iter__
,__next__
,close
,send
,throw
>>> data = [1, 2, 3]
>>>
>>> for number in data:
... print(number)
1
2
3
>>> data = [1, 2, 3]
>>> result = iter(data)
>>> try:
... while True:
... number = next(result)
... print(number)
... except StopIteration:
... pass
1
2
3
>>> data = [1, 2, 3]
>>> result = iter(data)
>>> try:
... number = next(result)
... print(number)
...
... number = next(result)
... print(number)
...
... number = next(result)
... print(number)
...
... number = next(result)
... print(number)
... except StopIteration:
... pass
1
2
3
>>> data = [1, 2, 3]
>>> result = data.__iter__()
>>> try:
... number = result.__next__()
... print(number)
...
... number = result.__next__()
... print(number)
...
... number = result.__next__()
... print(number)
...
... number = result.__next__()
... print(number)
... except StopIteration:
... pass
1
2
3
5.1.6. Builtins
Iterable:
__iter__
Iterator:
__iter__
,__next__
Generator:
__iter__
,__next__
,close
,send
,throw
Iterable:
>>> data = range(0,3)
>>>
>>> data
range(0, 3)
>>>
>>> hasattr(data, '__iter__')
True
>>> hasattr(data, '__next__')
False
>>>
>>> result = iter(data)
>>> hasattr(result, '__iter__')
True
>>> hasattr(result, '__next__')
True
>>>
>>> next(result)
0
>>> next(result)
1
>>> next(result)
2
>>> next(result)
Traceback (most recent call last):
StopIteration
Iterator:
>>> data = [1, 2, 3]
>>> result = reversed(data)
>>>
>>> hasattr(result, '__iter__')
True
>>> hasattr(result, '__next__')
True
>>>
>>> next(result)
3
>>> next(result)
2
>>> next(result)
1
>>> next(result)
Traceback (most recent call last):
StopIteration
External Iterators:
>>> data = [1, 2, 3]
>>>
>>> iter(data)
<list_iterator object at 0x106475390>
>>>
>>> reversed(data)
<list_reverseiterator object at 0x106474b50>
5.1.7. Case Study
History (like browser history)
Problem:
class Group:
def __init__(self):
self.members = []
def add_member(self, member):
self.members.append(member)
return self
def get_members(self):
return self.members
admins = Group()
admins.add_member('mwatney')
admins.add_member('mlewis')
admins.add_member('rmartinez')
for i in range(len(admins.get_members())):
member = admins.get_members()[i]
print(member)
# mwatney
# mlewis
# rmartinez
Solution:
class Group:
def __init__(self):
self.members = []
def add_member(self, member):
self.members.append(member)
return self
def __iter__(self):
self._current = 0
return self
def __next__(self):
if self._current >= len(self.members):
raise StopIteration
result = self.members[self._current]
self._current += 1
return result
admins = Group()
admins.add_member('mwatney')
admins.add_member('mlewis')
admins.add_member('rmartinez')
for member in admins:
print(member)
# mwatney
# mlewis
# rmartinez
Diagram:

5.1.8. Use Case - 1
from typing import Self
from dataclasses import dataclass, field
@dataclass
class Browser:
history: list[str] = field(default_factory=list)
def open(self, url: str) -> None:
self.history.append(url)
# return urlopen(url).read()
def __iter__(self) -> Self:
self._current = 0
return self
def __next__(self) -> str:
if self._current >= len(self.history):
raise StopIteration
result = self.history[self._current]
self._current += 1
return result
if __name__ == '__main__':
browser = Browser()
browser.open('https://python3.info')
browser.open('https://numpy.astrotech.io')
browser.open('https://pandas.astrotech.io')
browser.open('https://design-patterns.astrotech.io')
for url in browser:
print(url)
# https://python3.info
# https://numpy.astrotech.io
# https://pandas.astrotech.io
# https://design-patterns.astrotech.io
5.1.9. Use Case - 2
from urllib.request import urlopen
from dataclasses import dataclass, field
@dataclass
class Browser:
history: list[str] = field(default_factory=list)
def open(self, url: str) -> None:
self.history.append(url)
# return urlopen(url).read()
if __name__ == '__main__':
browser = Browser()
browser.open('https://python3.info')
browser.open('https://numpy.astrotech.io')
browser.open('https://pandas.astrotech.io')
browser.open('https://design-patterns.astrotech.io')
for url in browser.history:
print(url)
# https://python3.info
# https://numpy.astrotech.io
# https://pandas.astrotech.io
# https://design-patterns.astrotech.io
5.1.10. Use Case - 3
from dataclasses import dataclass, field
@dataclass
class BrowseHistory:
urls: list[str] = field(default_factory=list)
def push(self, url: str) -> None:
self.urls.append(url)
def pop(self) -> str:
return self.urls.pop()
def get_urls(self) -> list[str]:
return self.urls
if __name__ == '__main__':
history = BrowseHistory()
history.push(url='https://a.example.com')
history.push(url='https://b.example.com')
history.push(url='https://c.example.com')
for i in range(len(history.get_urls())):
url = history.get_urls()[i]
print(i)
from dataclasses import dataclass, field
class Iterator:
def has_next(self) -> bool:
raise NotImplementedError
def current(self) -> str:
raise NotImplementedError
def next(self) -> None:
raise NotImplementedError
@dataclass
class BrowseHistory:
urls: list[str] = field(default_factory=list)
def push(self, url: str) -> None:
self.urls.append(url)
def pop(self) -> str:
return self.urls.pop()
def get_urls(self) -> list[str]:
return self.urls
def create_iterator(self) -> Iterator:
return self.ListIterator(self)
@dataclass
class ListIterator(Iterator):
history: 'BrowseHistory'
index: int = 0
def has_next(self) -> bool:
return self.index < len(history.urls)
def current(self) -> str:
return history.urls[self.index]
def next(self) -> None:
self.index += 1
if __name__ == '__main__':
history = BrowseHistory()
history.push(url='https://a.example.com')
history.push(url='https://b.example.com')
history.push(url='https://c.example.com')
iterator = history.create_iterator()
while iterator.has_next():
url = iterator.current()
print(url)
iterator.next()
# https://a.example.com
# https://b.example.com
# https://c.example.com
5.1.11. Use Case - 4
Overload
__iadd__
operator
>>> class Group:
... def __init__(self):
... self.users = []
...
... def __iadd__(self, user):
... self.users.append(user)
... return self
>>>
>>>
>>> admins = Group()
>>> admins += 'alice'
>>> admins += 'bob'
>>> admins += 'carol'
>>>
>>> for user in admins.users:
... print(user)
alice
bob
carol
5.1.12. Assignments
# %% About
# - Name: DesignPatterns Behavioral Iterator
# - Difficulty: easy
# - Lines: 9
# - Minutes: 5
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% English
# 1. Implement Iterator pattern
# 2. Run doctests - all must succeed
# %% Polish
# 1. Zaimplementuj wzorzec Iterator
# 2. Uruchom doctesty - wszystkie muszą się powieść
# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> crew = Crew()
>>> crew += 'Mark Watney'
>>> crew += 'Jose Jimenez'
>>> crew += 'Melissa Lewis'
>>>
>>> for member in crew:
... print(member)
Mark Watney
Jose Jimenez
Melissa Lewis
"""
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% Imports
# %% Types
from typing import Callable
Crew: type
__iadd__: Callable[[object, str], object]
__iter__: Callable[[object], object]
__next__: Callable[[object], str]
# %% Data
# %% Result
class Crew:
def __init__(self):
self.members = list()
def __iadd__(self, other):
self.members.append(other)
return self
# %% About
# - Name: Protocol Iterator Implementation
# - Difficulty: easy
# - Lines: 9
# - Minutes: 3
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% English
# 1. Modify classes to implement iterator protocol
# 2. Iterator should return instances of `Group`
# 3. Run doctests - all must succeed
# %% Polish
# 1. Zmodyfikuj klasy aby zaimplementować protokół iterator
# 2. Iterator powinien zwracać instancje `Group`
# 3. Uruchom doctesty - wszystkie muszą się powieść
# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> from inspect import isclass, ismethod
>>> assert isclass(User)
>>> mark = User('Mark', 'Watney')
>>> assert hasattr(mark, 'firstname')
>>> assert hasattr(mark, 'lastname')
>>> assert hasattr(mark, 'groups')
>>> assert hasattr(mark, '__iter__')
>>> assert hasattr(mark, '__next__')
>>> assert ismethod(mark.__iter__)
>>> assert ismethod(mark.__next__)
>>> mark = User('Mark', 'Watney', groups=(
... Group(gid=1, name='admins'),
... Group(gid=2, name='staff'),
... Group(gid=3, name='managers'),
... ))
>>> for mission in mark:
... print(mission)
Group(gid=1, name='admins')
Group(gid=2, name='staff')
Group(gid=3, name='managers')
"""
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% Imports
from dataclasses import dataclass
# %% Types
from typing import Callable
User: type
__iter__: Callable[[object], object]
__next__: Callable[[object], object]
# %% Data
@dataclass
class Group:
gid: int
name: str
# %% Result
@dataclass
class User:
firstname: str
lastname: str
groups: tuple = ()
# %% About
# - Name: Protocol Iterator Range
# - Difficulty: medium
# - Lines: 9
# - Minutes: 8
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% English
# 1. Modify class `Range` to write own implementation
# of a built-in `range(start, stop, step)` function
# 2. Assume, that user will never give only one argument;
# it will always be either two or three arguments
# 3. Use Iterator protocol
# 4. Run doctests - all must succeed
# %% Polish
# 1. Zmodyfikuj klasę `Range` aby napisać własną implementację
# wbudowanej funkcji `range(start, stop, step)`
# 2. Przyjmij, że użytkownik nigdy nie poda tylko jednego argumentu;
# zawsze będą to dwa lub trzy argumenty
# 3. Użyj protokołu Iterator
# 4. Uruchom doctesty - wszystkie muszą się powieść
# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> from inspect import isclass, ismethod
>>> assert isclass(Range)
>>> r = Range(0, 0, 0)
>>> assert hasattr(r, '__iter__')
>>> assert hasattr(r, '__next__')
>>> assert ismethod(r.__iter__)
>>> assert ismethod(r.__next__)
>>> list(Range(0, 10, 2))
[0, 2, 4, 6, 8]
>>> list(Range(0, 5))
[0, 1, 2, 3, 4]
"""
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% Imports
from dataclasses import dataclass
# %% Types
from typing import Callable
Range: type
__iter__: Callable[[object], object]
__next__: Callable[[object], int]
# %% Data
# %% Result
@dataclass
class Range:
start: int = 0
stop: int = None
step: int = 1