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.
Python code
32 linesfrom 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
{'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
- Use a base64-encoded cursor to make the token opaque, e.g., `base64.urlsafe_b64encode(f'{page}:{sort_key}'.encode())`.
- 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
More from API design & gRPC
- Build a Bulk Array POST Mock Server in Python medium
- Build a Mock REST API with PUT and GET in Python medium
- Convert Protobuf to JSON and Dict in Python easy
- Create a Data Helper in Python for gRPC-style APIs easy
- Format data in Python using dataclasses like gRPC messages easy
- Generate an OpenAPI Spec from Mock Routes in Python easy
Keep learning
Related tutorials and quizzes for this topic.