In a Django function view, how do you paginate a QuerySet with Paginator, and what does the template loop over?
answer
- two objects: book and page
- per_page in the constructor
- the forgiving page lookup
- has_next and next_page_number
basics
~10 sWrap 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 linesfrom 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
Recall the pair: Paginator(queryset, per_page) for the whole list, get_page() for one Page, and a template loop over page_obj.
Explain the Page helpers for navigation and the two queries a paginated QuerySet page costs: a COUNT and a sliced SELECT.
Anticipate where it hurts: the COUNT on huge tables, deep offsets, and why the QuerySet must be ordered before paginating.
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