Card 04/ 09

ExampleDifficulty: Intermediate2 min

Iterator Example: Paging Through API Results

The caller loops over orders; the iterator fetches the next page when the current one runs out.

Results fetched from a server a page at a time are a natural Iterator, and this example maps the template onto them. An order history holds hundreds of orders, and the server returns a few per request, with a token for the next page. The caller should loop over orders and never see a page or a token. The pages here are kept in a dictionary, so the listing runs without a network.

The template's parts in the paging example
TemplatePaging example
ConcreteAggregateOrderHistory
ConcreteIteratorOrderPageIterator
PositionThe current page, the position within it, and the next page's token
End of the collectionA page with no next token whose orders have all been returned
py
from collections.abc import Iterator

PAGES = {
    None: (["order-1", "order-2"], "p2"),
    "p2": (["order-3", "order-4"], "p3"),
    "p3": (["order-5"], None),
}


def fetch_page(token: str | None) -> tuple[list[str], str | None]:
    print(f"  fetching page {token or 'p1'}")
    return PAGES[token]


class OrderHistory:
    def __iter__(self) -> "OrderPageIterator":
        return OrderPageIterator()


class OrderPageIterator(Iterator[str]):
    def __init__(self) -> None:
        self._page, self._next_token = fetch_page(None)
        self._position = 0

    def __next__(self) -> str:
        if self._position == len(self._page):
            if self._next_token is None:
                raise StopIteration
            self._page, self._next_token = fetch_page(self._next_token)
            self._position = 0
        order = self._page[self._position]
        self._position += 1
        return order


for order in OrderHistory():
    print(order)
OutputRan with python:3.12-slim (Python 3.12.13)
  fetching page p1
order-1
order-2
  fetching page p2
order-3
order-4
  fetching page p3
order-5
A page is fetched only when the previous one has been used up.

The loop at the bottom is an ordinary for, and prints five orders. The indented lines show when each page is fetched: page two is requested only after the second order has been returned, and page three after the fourth. A caller that stops after the first order would never cause the second request.

The position is now three values instead of one index, and none of them reaches the caller. If the server changed from page tokens to numbered pages, OrderPageIterator would change and the loop would not.