Implementing Pagination
To split large datasets across multiple pages, use the Paginator class for synchronous code or AsyncPaginator for asynchronous environments. These classes handle the logic of slicing your data and providing navigation metadata.
Synchronous Pagination
To paginate a list or QuerySet in a synchronous view, instantiate Paginator with your data and the number of items per page.
from django.core.paginator import Paginator
# Example data
objects = ['john', 'paul', 'george', 'ringo']
paginator = Paginator(objects, 2)
# Get metadata
print(paginator.count) # 4
print(paginator.num_pages) # 2
print(paginator.page_range) # range(1, 3)
# Retrieve a specific page
page1 = paginator.page(1)
print(page1.object_list) # ['john', 'paul']
print(page1.has_next()) # True
print(page1.next_page_number()) # 2
The Page object returned by paginator.page() acts as a sequence of the items on that page. It provides helper methods like has_previous(), has_next(), and start_index() to build navigation links in your templates.
Asynchronous Pagination
When working in an async context, use AsyncPaginator. This class provides asynchronous versions of the standard paginator methods, prefixed with a.
from django.core.paginator import AsyncPaginator
async def get_async_items():
objects = [1, 2, 3, 4, 5]
paginator = AsyncPaginator(objects, 2)
# Await metadata methods
count = await paginator.acount()
num_pages = await paginator.anum_pages()
# Retrieve an AsyncPage
page = await paginator.apage(1)
# IMPORTANT: You must await aget_object_list() before
# treating the page as a list (indexing or len())
items = await page.aget_object_list()
print(items) # [1, 2]
# Alternatively, iterate directly over the page
async for obj in page:
print(obj)
Safe Page Retrieval
To handle user input safely without manually catching PageNotAnInteger or EmptyPage exceptions, use get_page() (sync) or aget_page() (async). These methods default to page 1 if the input is invalid and to the last page if the number is out of range.
# Synchronous safe retrieval
page = paginator.get_page(request.GET.get('page'))
# Asynchronous safe retrieval
page = await async_paginator.aget_page(request.GET.get('page'))
Handling "Orphans"
The orphans parameter allows you to prevent a very small number of items from appearing on the final page. If the last page would have fewer than or equal to the orphans count, those items are added to the previous page instead.
# If there are 21 items and per_page=10, orphans=2:
# Page 1: 10 items
# Page 2: 11 items (the 1 orphan is rolled into this page)
paginator = Paginator(my_queryset, 10, orphans=2)
Note: In this codebase, the
orphansargument must be smaller than theper_pageargument. Using a value larger than or equal toper_pageis deprecated and will raise aValueErrorin future versions.
Generating Elided Page Ranges
For datasets with many pages, you can generate a range that includes ellipses to keep the navigation bar manageable using get_elided_page_range.
paginator = Paginator(range(100), 10)
# Returns a generator: [1, 2, '…', 7, 8, 9, 10]
page_range = paginator.get_elided_page_range(number=8, on_each_side=2, on_ends=1)
Troubleshooting
Unordered QuerySets
If you pass an unordered QuerySet to a paginator, it will issue an UnorderedObjectListWarning. Pagination requires a consistent order to ensure items do not skip pages or appear twice. Always use .order_by() on your QuerySets before paginating.
# Warning: Pagination may yield inconsistent results
paginator = Paginator(MyModel.objects.all(), 20)
# Correct:
paginator = Paginator(MyModel.objects.order_by('id'), 20)
AsyncPage Indexing
A common error when using AsyncPage is attempting to use len(page) or page[0] before the data is loaded. You must await page.aget_object_list() first:
page = await async_paginator.apage(1)
# This raises TypeError:
# length = len(page)
# Do this instead:
await page.aget_object_list()
length = len(page)