8.1. Iterator

  • EN: Iterator

  • PL: Iterator

  • Type: object

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.

Here's a simple example of an Iterator in Python:

>>> class MyIterator:
...     def __init__(self, data):
...         self.data = data
...
...     def __iter__(self):
...         self._current = 0
...         return self
...
...     def __next__(self):
...         if self._current >= len(self.data):
...             raise StopIteration
...         result = self.data[self._current]
...         self._current += 1
...         return result

This will output:

>>> my_data = [1, 2, 3, 4, 5]
>>> my_iterator = MyIterator(my_data)
>>> for item in my_iterator:
...     print(item)
...
1
2
3
4
5

8.1.1. Pattern

  • History (like browser history)

8.1.2. 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

8.1.3. Solution

../../_images/designpatterns-iterator-solution.png
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

8.1.4. Use Case - 1

from urllib.request import urlopen
from dataclasses import dataclass, field
from typing import Self


@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

8.1.5. 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

8.1.6. 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

8.1.7. Assignments

# %% 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

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: DesignPatterns Behavioral Iterator
# - Difficulty: easy
# - Lines: 9
# - Minutes: 5

# %% English
# 1. Implement Iterator pattern
# 2. Run doctests - all must succeed

# %% Polish
# 1. Zaimplementuj wzorzec Iterator
# 2. Uruchom doctesty - wszystkie muszą się powieść

# %% Tests
"""
>>> 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
"""


class Crew:
    def __init__(self):
        self.members = list()

    def __iadd__(self, other):
        self.members.append(other)
        return self


# %% 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

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: Protocol Iterator Implementation
# - Difficulty: easy
# - Lines: 9
# - Minutes: 3

# %% 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ść

# %% Tests
"""
>>> 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')
"""

from dataclasses import dataclass


@dataclass
class User:
    firstname: str
    lastname: str
    groups: tuple = ()


@dataclass
class Group:
    gid: int
    name: str


# %% 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

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% About
# - Name: Protocol Iterator Range
# - Difficulty: medium
# - Lines: 9
# - Minutes: 8

# %% 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ść

# %% Tests
"""
>>> 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]
"""
from dataclasses import dataclass


@dataclass
class Range:
    start: int = 0
    stop: int = None
    step: int = 1