We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Laziness, Iteration and Pipelines step 6 of 14
Async iterators: the paginator you can actually close
Breaking out of an async for leaves the iterator suspended. For an async
generator, its finally block then runs whenever the event loop gets round to
finalising it — possibly during loop shutdown, possibly long after the
connection you meant to release was needed by someone else. “It runs
eventually” is not a resource-management strategy.
contextlib.aclosing (3.10) makes it deterministic: the aclose() happens at
the closing brace of the async with, synchronously with respect to your code.
Two protocol facts worth memorising:
-
__aiter__must not beasync def. It is a plain method that returns the async iterator. Returning an awaitable from it was deprecated in 3.5.2 and removed in 3.7. Writeasync def __aiter__today andasync forraises aTypeErrorat the first iteration. -
aiter()andanext()are builtins as of 3.10, andanext(it, default)is the async analogue ofnext(it, default): it returns the default instead of raisingStopAsyncIteration.
What to write
The three protocol methods of Paginator[T]. __init__, the fetch callable,
and the driver are provided.
class Paginator[T]:
def __init__(self, fetch: Callable[[int], Awaitable[list[T]]]) -> None: ...
def __aiter__(self) -> Self: ...
async def __anext__(self) -> T: ...
async def aclose(self) -> None: ...
-
__aiter__returnsself. Notasync def. -
__anext__serves the next buffered item. When the buffer is empty it awaitsself._fetch(self._page)for the next page and advances_page. An empty page ends the stream — set_exhaustedand raiseStopAsyncIteration. A page in the middle that comes back empty also ends it; that is the documented contract of this API, not an accident. -
aclose()marks the paginator closed and drops the buffer. Every later__anext__must raiseStopAsyncIteration— which is what makesanext(paginator, sentinel)return the sentinel.
What the report proves
-
items— the flattened pages, in order. -
fetch_calls— how many times the network was actually touched. Withstop_after=2over a first page of two items, this must be 1: a paginator that prefetches the next page while you are still consuming the current one is a paginator that costs money for rows nobody read. -
closed—Trueafter theasync with aclosing(...)block, on every path, including an earlybreak. -
after_close— the value ofawait anext(paginator, sentinel)once the paginator is closed or exhausted. It must be the sentinel.
Types
Self (PEP 673, 3.11) is the correct return annotation for __aiter__; a
subclass then gets the subclass type back rather than the base. Annotating it
AsyncIterator[T] would compile but throws that away.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.