Chapter 16 · Caching, files, mail, signals and tasks

16.14 Pagination: Paginator and Page

How Paginator cuts a list or a queryset into numbered pages: the count, orphans, the check of a page number, the slice and the Page; and its asynchronous twins, AsyncPaginator and AsyncPage.

A paginator is given a whole list and a page size, and must find any page’s objects without loading the list. A count and a slice suffice, and on a queryset each is one query: a count, and a SELECT with a LIMIT and, past the first page, an OFFSET. What the two paginators share is in BasePaginator. ListView makes one when it is given a page size (The generic views).

Making a paginator

BasePaginator.__init__ keeps the list; the page size and the number of orphans, objects that may join the last page rather than make one of their own, both through int; whether the first page may be empty, allow_empty_first_page=True by default; and BasePaginator.default_error_messages with any given in error_messages= merged over them by key.

On a queryset each page is its own query. Where the list’s ordered, which a queryset has and a list lacks, is false, BasePaginator._check_object_list_is_ordered warns with UnorderedObjectListWarning that pagination “may yield inconsistent results”. The test is of QuerySet.ordered, not QuerySet.totally_ordered, so an ordering by a column whose values repeat passes it (Annotations, ordering and the other methods that change the query).

Counting and the number of pages

Paginator.count calls the list’s count where it is callable, not built in, and takes no argument, as QuerySet.count does, and otherwise takes len. The built-in test turns away a list’s own count, which method_has_no_args, not seeing a built-in as a method, would accept.

count and Paginator.num_pages are cached properties. num_pages is 0 for an empty list with allow_empty_first_page=False, and otherwise the count less the orphans, at least 1, divided by the page size and rounded up: an empty list has one empty page, and orphans add no page. Paginator.page_range runs from 1 to the number of pages; iterating a paginator yields its pages.

A page by its number: page and get_page

Paginator.page checks the number and slices one page size from (number − 1) × page size; where the slice’s end plus the orphans reaches the count, the slice runs to the end, and the orphans join the last page. BasePaginator._get_page, a hook for a subclass that wants another class, wraps the slice in a Page. Slicing a queryset not yet fetched only sets limits on a copy’s query (Evaluation and the result cache), so the rows wait until the page is read.

Paginator.validate_number hands the number and the number of pages to BasePaginator._validate_number, which returns the number through int, or raises a subclass of InvalidPage with one of the paginator’s error messages, the defaults marked for translation:

the number raises key of the message
a float with a fraction, or what int refuses (1.5, "2.0", None) raises PageNotAnInteger: That page number is not an integer key of the message "invalid_page"
less than 1 raises EmptyPage: That page number is less than 1 key of the message "min_page"
above the number of pages raises EmptyPage: That page contains no results key of the message "no_results"

Paginator.get_page takes a number as a request gave it: PageNotAnInteger becomes the first page, EmptyPage the last. With allow_empty_first_page=False and nothing to page it still raises EmptyPage: there is no page 1, and the last page is then page 0.

Ten berths at Valletta, numbered 1 to 10, four to a page, then with two orphans, with the SQL each line sent:

berths = Berth.objects.order_by("number")
paginator = Paginator(berths, 4)
paginator.num_pages  ->  3
  SELECT COUNT(*) AS "__count" FROM "office_berth"
page = paginator.page(2)  ->  <Page 2 of 3>   (0 queries)
[berth.number for berth in page]  ->  [5, 6, 7, 8]
  SELECT "office_berth"."port_id", "office_berth"."number", "office_berth"."metres" FROM "office_berth" ORDER BY "office_berth"."number" ASC LIMIT 4 OFFSET 4
page[0]  ->  <Berth: Berth object ((1, 5))>   (0 queries)
paginator.page(4)  ->  raises EmptyPage: That page contains no results
paginator.get_page(4)  ->  <Page 3 of 3>
paginator.get_page("four")  ->  <Page 1 of 3>

paginator = Paginator(berths, 4, orphans=2)
paginator.num_pages  ->  2
  SELECT COUNT(*) AS "__count" FROM "office_berth"
page = paginator.page(2)  ->  <Page 2 of 2>
[berth.number for berth in page]  ->  [5, 6, 7, 8, 9, 10]
  SELECT "office_berth"."port_id", "office_berth"."number", "office_berth"."metres" FROM "office_berth" ORDER BY "office_berth"."number" ASC LIMIT 6 OFFSET 4
page.start_index(), page.end_index()  ->  (5, 10)

Page

Page is a collections.abc.Sequence, which builds iteration, in and reversed on its __len__ and __getitem__. The first time Page.__getitem__ is used it turns the page’s objects into a list, so that, its comment says, a queryset is not a query for each index. Iteration goes through it, so the rows are fetched once; the run’s page[0] sent nothing.

Page.has_next compares the number with the paginator’s num_pages; Page.next_page_number and Page.previous_page_number go through validate_number, so past either end they raise EmptyPage. Page.start_index and Page.end_index count from 1 across the list: the start is 0 for an empty list, and the last page ends at the count, orphans included.

The elided page range

Paginator.get_elided_page_range gives the numbers for a long row of links, with BasePaginator.ELLIPSIS, a translatable "…", for what it leaves out. It keeps on_ends=2 pages at each end and on_each_side=3 around the current one, gives every page when there are no more than twice their sum, and never elides a single page.

AsyncPaginator and AsyncPage

AsyncPaginator takes the same arguments and has an asynchronous twin of each method, named with an a in front (AsyncPaginator.apage), and acount, anum_pages and apage_range for the properties. AsyncPaginator.acount awaits the list’s acount where it is a coroutine function with no argument, as QuerySet.acount is, and otherwise takes len.

AsyncPage is not a sequence. Its __len__, __getitem__ and __reversed__ are synchronous and fetch nothing: while its objects are not a list they raise TypeError, until AsyncPage.aget_object_list makes them one, by async for where the list allows it and otherwise by list under sync_to_async. An async for over the page needs no such step.

page = await AsyncPaginator(berths, 4).apage(3)  ->  <Async Page 3>
len(page)  ->  raises TypeError: AsyncPage.aget_object_list() must be awaited before calling len().