CodeRigor

LESSON

Paginate without loops or conflicting duplicates

Follow opaque cursors, bound pages/items, deduplicate identical records and reject conflicting versions.

A page can be valid while a collection is wrong. The next cursor can loop. Boundary records can overlap. A retry or concurrent update can return the same id with a different amount. Infinite requests and double-counted money are both bugs.

The collector uses a supplied fetch_page(cursor) function. That makes pagination testable without a socket. The desktop lab will connect it to request encoding, bounded reads and retry policy.

Follow data, not cursor arithmetic
start / 3
Start with a None cursor, fetch a validated page, then either finish at next None or follow its opaque token under a page budget.
Follow data, not cursor arithmetic
Start with a None cursor, fetch a validated page, then either finish at next None or follow its opaque token under a page budget.

None has two roles in this endpoint protocol: it starts the first request, and a returned next None ends pagination. A loop that checks while cursor is not None before the first fetch would skip the entire API.

Before each fetch, check cancellation, cursor history and max_pages. Even a page with no items counts as work. Mark the cursor before calling the supplied fetcher so a repeated token cannot trigger another request.

Python
pages = {None:{"ids":["a"],"next":"opaque:+0"}, "opaque:+0":{"ids":["a","b"],"next":None}}
cursor, calls = None, []
while True:
    calls.append(cursor)
    page = pages[cursor]
    cursor = page["next"]
    if cursor is None: break
print(calls)

Merge records by stable id. Equal repeated records are ignored, preserving first-seen order. Different records with the same id raise id-conflict. This lab chooses rejection rather than guessing which version is newer. A real API with version timestamps may support another explicit reconciliation policy.

max_items counts unique accepted records, so a harmless duplicate does not consume another slot. Check the limit before inserting a new id. Keep accepted dictionaries independent of the raw page.

Cancellation is cooperative. Check before fetching and again after a result arrives, before accepting it. The second guard prevents stale acceptance, but it does not make an already sent request disappear. Later concurrency lessons retain that distinction.

Stable identity protects the report
start / 3
An unseen id adds one bounded record; an equal duplicate is ignored; a conflicting duplicate ends with a clear failure.
Stable identity protects the report
An unseen id adds one bounded record; an equal duplicate is ignored; a conflicting duplicate ends with a clear failure.
Python
records = {}
for record in [{"id":"a","cents":125},{"id":"a","cents":125},{"id":"b","cents":200}]:
    old = records.get(record["id"])
    if old is not None and old != record:raise ValueError("conflict")
    if old is None:records[record["id"]] = dict(record)
print(list(records))
print(sum(r["cents"] for r in records.values()))

Complete merging and termination. Test an empty terminal page, duplicate overlap, a conflicting id, a repeated cursor, a page-budget stop and cancellation immediately after a fetch. Inspect fetch call counts as well as returned values.

Next we decide which individual failed requests may be retried, and how much deliberate waiting that policy may spend.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Optional HTTP background using Requests. Our original lab uses the standard library and an injected transport; no Requests dependency is required.