How to Build Cursor Pagination with Next and Prev Tokens in Python

A minimal cursor pagination implementation that returns next and previous cursor tokens for navigating a dataset.

Medium Python 3.9+ Aug 9, 2026 API design & gRPC 15 views 0 copies

Python code

32 lines
Python 3.9+
from pprint import pprint


def make_cursor(page):
    return f"page:{page:04d}"


def parse_cursor(cursor):
    _, page = cursor.split(":", 1)
    return int(page)


def paginate(all_items, page_size, cursor=None):
    start = parse_cursor(cursor) if cursor else 0
    end = start + page_size
    items = all_items[start:end]
    next_cursor = make_cursor(end) if end < len(all_items) else None
    prev_cursor = make_cursor(max(0, start - page_size)) if start > 0 else None
    return {
        "items": items,
        "next_cursor": next_cursor,
        "prev_cursor": prev_cursor,
    }


if __name__ == "__main__":
    data = [f"item-{i}" for i in range(10)]
    result = paginate(data, page_size=5)
    pprint(result)

    result2 = paginate(data, page_size=5, cursor=result["next_cursor"])
    pprint(result2)

Output

stdout
{'items': ['item-0', 'item-1', 'item-2', 'item-3', 'item-4'],
 'next_cursor': 'page:0005',
 'prev_cursor': None}
{'items': ['item-5', 'item-6', 'item-7', 'item-8', 'item-9'],
 'next_cursor': None,
 'prev_cursor': 'page:0000'}

How it works

The make_cursor function encodes the page offset into an opaque token string, while parse_cursor decodes it back. paginate slices the list from the cursor's page offset and generates a next_cursor when more items remain after the current page, and a prev_cursor when the start offset is greater than zero. This pattern avoids the classic offset-based pagination problem where items inserted or deleted shift the page boundaries, providing stable position-based navigation. The tokens are deliberately opaque to clients, so they can be passed back in API requests without exposing internal pagination logic.

Common mistakes

  • Sending the raw page number in the cursor string without an opaque prefix, leaking database offsets to clients.
  • Forgetting to handle the case where the cursor points past the end of the data, leading to an empty page or an out-of-range error.
  • Not setting `prev_cursor` to None on the first page, causing clients to attempt navigating to a non-existent previous page.
  • Using the same cursor format for both directions (next and prev) without storing the direction in the token, making it ambiguous.

Variations

  1. Use a base64-encoded cursor to make the token opaque, e.g., `base64.urlsafe_b64encode(f'{page}:{sort_key}'.encode())`.
  2. Implement cursor pagination with a sort key like an ID or timestamp, using `WHERE id > ? ORDER BY id LIMIT ?` in a database query.

Real-world use cases

  • Pagination in a REST API endpoint that lists user-created records with a stable order across inserts and deletes.
  • Infinite-scroll feeds in a web app where the cursor token is returned in the JSON response and sent in the next request's query parameter.
  • Batch processing job that iterates over all rows in a table, using a cursor to continue from the last processed position after a restart or failure.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from API design & gRPC

Related tutorials and quizzes for this topic.