skip to content

Paginator & Pages

django.core.paginator splits a list or QuerySet into Page objects, and ListView wires it in via paginate_by. Interviewers probe the unordered-QuerySet warning and COUNT(*) cost on big tables.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

5

In a Django function view, how do you paginate a QuerySet with Paginator, and what does the template loop over?

level: juniorimportance: must knowfreq 55%

answer

  1. two objects: book and page
  2. per_page in the constructor
  3. the forgiving page lookup
  4. has_next and next_page_number

basics

~10 s

Wrap the ordered QuerySet in Paginator(queryset, per_page), fetch the requested page with get_page(request.GET.get('page')), and pass that Page to the template, which loops over it and uses has_next() and next_page_number() for links.

solid answer

~30 s

`django.core.paginator.Paginator(object_list, per_page)` knows the whole list: `count`, `num_pages` and `page_range`. Asking it for one page returns a `Page`, which holds only that slice in `object_list` plus navigation helpers: `has_next()`, `has_previous()`, `next_page_number()`, `previous_page_number()`, `start_index()` and `end_index()`, with `page.paginator` pointing back to the book. In a function view I call `paginator.get_page(request.GET.get('page'))`, which accepts junk and out-of-range numbers, and pass the page as `page_obj`. The template iterates `{% for entry in page_obj %}` and builds `?page=` links from those helpers. For a QuerySet, the page is a slice, so only that page's rows are fetched, plus a `COUNT` query for the total.

code

python · 11 lines
python
from django.core.paginator import Paginator
from django.shortcuts import render

from .models import AuditEntry


def audit_log(request):
    entries = AuditEntry.objects.order_by('-created_at', '-id')
    paginator = Paginator(entries, 50)
    page_obj = paginator.get_page(request.GET.get('page'))
    return render(request, 'audit/log.html', {'page_obj': page_obj})

go deeper

for a junior

Recall the pair: Paginator(queryset, per_page) for the whole list, get_page() for one Page, and a template loop over page_obj.

for a middle

Explain the Page helpers for navigation and the two queries a paginated QuerySet page costs: a COUNT and a sliced SELECT.

for a senior

Anticipate where it hurts: the COUNT on huge tables, deep offsets, and why the QuerySet must be ordered before paginating.

for a principal

Standardise one pagination include and one ordering rule for all list pages so function and class-based views behave the same.

## The two objects Django's pagination lives in `django.core.paginator` and is independent of views and templates. It has two main classes: | Class | Represents | Useful members | |---|---|---| | `Paginator` | the whole list, split into numbered pages | `count`, `num_pages`, `page_range`, `get_page()`, `page()` | | `Page` | one page of it | `object_list`, `number`, `has_next()`, `has_previous()`, `next_page_number()`, `previous_page_number()`, `start_index()`, `end_index()`, `paginator` | The constructor is `Paginator(object_list, per_page, orphans=0, allow_empty_first_page=True, error_messages=None)`. The list can be a Python list or tuple, or anything sliceable with `count()` or `__len__()`, and in practice it is usually a `QuerySet`. ## Paginating an audit log in a function view An admin screen lists entries from an audit log table. The steps are: 1. Build the QuerySet with an explicit, deterministic order: `AuditEntry.objects.order_by('-created_at', '-id')`. 2. Wrap it: `paginator = Paginator(entries, 50)`. 3. Read the requested number from the query string and ask for the page: `page_obj = paginator.get_page(request.GET.get('page'))`. 4. Render with `{'page_obj': page_obj}` in the context. `get_page()` is the forgiving lookup: a missing or non-numeric value gives page 1, and a number past the end gives the last page. That suits a human-facing HTML list, where a stale bookmark should still show something. ## What the template uses - `{% for entry in page_obj %}` iterates the entries on this page only. `Page` behaves as a sequence, so `len(page_obj)` and indexing work too. - `page_obj.has_previous` and `page_obj.previous_page_number` build the "newer" link; `has_next` and `next_page_number` build the "older" link. - `page_obj.number` and `page_obj.paginator.num_pages` print "page 3 of 40,000". - `page_obj.start_index` and `end_index` print "entries 101-150" with 1-based positions. - `paginator.get_elided_page_range()` gives a compact page list with ellipses for very long ranges. Call `next_page_number()` only after `has_next()` is true; on the last page it raises `EmptyPage`. ## What runs against the database With a QuerySet, pagination stays lazy until needed: - `Paginator.count` calls `QuerySet.count()`, one `SELECT COUNT(*)` over the filtered rows. It is needed to validate the page number and to compute `num_pages`, and is cached on the paginator instance for the request. - `page.object_list` is the QuerySet sliced to `[bottom:top]`, which becomes a `LIMIT`/`OFFSET` query when the template iterates it. So a paginated page costs two queries, however large the table. That count becomes the expensive part on very large tables, which is a separate concern. ## Common mistakes - **Looping over the wrong object.** Iterating `paginator.object_list` or the original QuerySet renders every row; iterate the `Page`. - **Forgetting the ordering.** Without `order_by()` or a model default ordering, Django emits a warning because pages may overlap. - **Dropping filters from links.** A `?page=3` link that omits the current `?actor=` filter jumps to page 3 of a different list; carry the other query parameters into the pagination links. - **Passing a list.** `list(queryset)` makes every page load the whole table first. ## Class-based views `ListView` does the same wiring when you set `paginate_by`, and puts `paginator`, `page_obj` and `is_paginated` into the context. The template side is identical, so a function view and a class-based view can share one pagination include.

  • Does Django's Paginator load all two million audit rows to build one page?
    No, not for a QuerySet. It runs `count()` for the total and slices the QuerySet for the page, so the database returns only that page's rows. A plain Python list is different: it is already in memory, and `count` falls back to `len()`.
  • What does next_page_number() do on the last page?
    It raises `EmptyPage`, because it validates the number it would return. That is why templates call it only inside an `{% if page_obj.has_next %}` block.

saying these in an interview costs you the question

  • Paginator fetches every row and slices the list in Python
  • The template should loop over paginator.object_list
  • get_page() raises an error for a non-numeric page parameter
  • Page numbers in Django's Paginator start at zero
open as a page

In Django's Paginator, how do get_page() and page() differ, and which exceptions can page() raise?

level: middleimportance: should knowfreq 42%

basics

~10 s

page() is strict: it raises PageNotAnInteger for a non-integer and EmptyPage for a number out of range, both subclasses of InvalidPage. get_page() catches those, returning page 1 or the last page instead.

open as a page

What does setting paginate_by on a Django ListView add to the template context, and which attributes tune the pagination?

level: middleimportance: should knowfreq 48%

basics

~10 s

With paginate_by set, ListView builds a Paginator and adds paginator, page_obj and is_paginated, and object_list becomes just the current page. page_kwarg, paginate_orphans, allow_empty and paginator_class tune it.

open as a page

Why does Django emit UnorderedObjectListWarning when paginating some QuerySets, and why is ordering by created_at alone still not enough?

level: middleimportance: should knowfreq 38%

basics

~20 s

Without ORDER BY, the database may return rows in any order, so LIMIT/OFFSET pages can repeat or skip rows; Paginator warns when QuerySet.ordered is False. Ties on a non-unique column cause the same problem, so add the primary key as a tie-breaker.

open as a page

A Django ListView paginating two million audit-log rows is slow even on page 1; how do you find the cost and reduce it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Every paginated page runs Paginator.count, a SELECT COUNT(*) over all matching rows, before fetching the slice. On two million rows the count dominates; override count in a Paginator subclass (capped, cached or estimated) and set paginator_class, or drop page numbers.

open as a page