Focused practice · 8 min read

Python Pagination Exercises: Queues, Visits, and Limits

Pagination is a graph walk with a budget. A queue decides what comes next, a visited set prevents loops, and a limit keeps the run finite. These exercises build each piece separately before combining them.

Use a FIFO queue

A deque with append and popleft gives breadth-first order. That is usually what you want for pagination: finish the current listing level before following detail pages. A plain list with pop(0) is quadratic for large frontiers.

from collections import deque

queue = deque(["/page/1"])
seen = []
while queue:
    url = queue.popleft()
    seen.append(url)
    if url == "/page/1":
        queue.extend(["/page/2", "/page/3"])
print(seen)

First in, first out

Mark a page when you enqueue it

If you wait until a page is processed to mark it visited, two parents can enqueue the same child. Add the URL to visited at enqueue time, and check before every append. The set and the queue then agree on what is pending.

queue = ["/a"]
visited = {"/a"}
links = {"/a": ["/b", "/b", "/c"], "/b": ["/a"]}
order = []
while queue:
    page = queue.pop(0)
    order.append(page)
    for link in links.get(page, []):
        if link not in visited:
            visited.add(link)
            queue.append(link)
print(order)

No duplicate work

Spend a page budget

A budget makes the crawl reproducible and prevents an accidental infinite run. Decrement before parsing or after a successful fetch, but document which one. Stop cleanly when the budget reaches zero and report that the frontier is unfinished.

queue = ["/1", "/2", "/3", "/4"]
budget = 2
done = []
while queue and budget:
    done.append(queue.pop(0))
    budget -= 1
print(done)
print("remaining", len(queue))

A finite run

Keep the next link explicit

Many sites paginate with a rel=next link rather than a predictable number. Parse that link and resolve it against the current page. Do not guess /page/2 from /page/1 unless the site documents that pattern.

from html.parser import HTMLParser
from urllib.parse import urljoin

class Next(HTMLParser):
    def __init__(self):
        super().__init__()
        self.href = None
    def handle_starttag(self, tag, attrs):
        values = dict(attrs)
        if tag == "a" and values.get("rel") == "next" and self.href is None:
            self.href = values.get("href")

p = Next()
p.feed('<a rel="next" href="/page/2">Next</a>')
print(urljoin("https://example.com/page/1", p.href))

Follow the declared next link

Make output order deterministic

Sets are useful for membership, not for stable output. Convert visited URLs to a sorted list when writing a report, or keep a separate insertion-ordered list. Deterministic output makes diffs and tests meaningful.

visited = {"/b", "/a", "/c"}
print(sorted(visited))
print(list(dict.fromkeys(["/b", "/a", "/b", "/c"])))

Sort before reporting

Your practice checklist

Common questions

Breadth-first or depth-first?

For pagination, breadth-first usually matches the site structure and surfaces listing pages early. Choose depth-first only when you need one branch completely.

Why not store visited as a list?

Membership checks become slow as the list grows. A set gives constant-time lookup; keep a separate list if you need order.

How do I resume a crawl?

Persist the frontier, the visited set, and the budget together. JSON lines or a small database both work; the key is to save them atomically.

Practice pagination

12 questions on queues, visited sets, budgets, next links, and stable output.

Open practice

Keep reading: