skip to content

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

level: middleimportance: should knowfreq 42%

answer

  1. forgiving versus strict
  2. two subclasses of one exception
  3. what ListView turns them into
  4. the special 'last' value

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.

solid answer

~40 s

`Paginator.page(number)` validates first: a value `int()` cannot convert raises `PageNotAnInteger`; a number below 1 or above `num_pages` raises `EmptyPage`. Both inherit from `InvalidPage`, so one `except InvalidPage` catches either. `get_page(number)` wraps `page()`: `PageNotAnInteger` becomes page 1 and `EmptyPage` becomes the last page. It still raises `EmptyPage` in one case, an empty list with `allow_empty_first_page=False`. I use `get_page()` for HTML lists people click through, and `page()` when a bad number should be an error, for example a 404 so crawlers do not index endless duplicates of the last page. `ListView` takes the strict route: it calls `page()`, accepts `last` as a special value, and turns any other bad value into `Http404`.

code

python · 16 lines
python
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.http import Http404
from django.shortcuts import render

from .models import AuditEntry


def audit_log_strict(request):
    paginator = Paginator(AuditEntry.objects.order_by('-created_at', '-id'), 50)
    try:
        page_obj = paginator.page(request.GET.get('page', 1))
    except PageNotAnInteger:
        raise Http404('Page must be a number')
    except EmptyPage:
        raise Http404('No such page')
    return render(request, 'audit/log.html', {'page_obj': page_obj})

go deeper

for a junior

Remember that get_page() is forgiving and page() raises PageNotAnInteger or EmptyPage on bad input.

for a middle

Explain how get_page() wraps page(), the InvalidPage hierarchy, and ListView's strict handling with the 'last' special case and Http404.

for a senior

Choose per surface: clamping for people, errors for crawlers and APIs, and spot the empty-list edge where get_page() still raises.

for a principal

Pick one out-of-range policy for the whole site and make function and class-based lists follow it, so links behave predictably.

## Two ways to ask for a page `django.core.paginator.Paginator` offers two lookups that return the same `Page` object for a valid number and differ only in how they handle bad input. | Input | `page(number)` | `get_page(number)` | |---|---|---| | `'3'` or `3` (valid) | page 3 | page 3 | | `'abc'`, `None`, `'2.5'` | raises `PageNotAnInteger` | page 1 | | `0` or `-1` | raises `EmptyPage` | last page | | `999` past the end | raises `EmptyPage` | last page | | empty list, `allow_empty_first_page=False` | raises `EmptyPage` | raises `EmptyPage` | ## The exception hierarchy All three exception classes live in `django.core.paginator`: - `InvalidPage`, the base class; - `PageNotAnInteger(InvalidPage)`, when the value cannot be turned into an integer (including a float with a fractional part); - `EmptyPage(InvalidPage)`, when the integer is below 1 or above `num_pages`. Catch the subclass when you want different handling for each, or `InvalidPage` for both. Since Django 5.0 the messages can be customised with the paginator's `error_messages` argument. ## How get_page() is built `get_page()` is a thin wrapper, which explains its behaviour exactly: 1. Try `validate_number(number)`. 2. On `PageNotAnInteger`, use 1. 3. On `EmptyPage`, use `num_pages`. 4. Return `page(number)`. Step 4 can still raise: when the list is empty and `allow_empty_first_page=False`, `num_pages` is 0, and `page(0)` raises `EmptyPage`. With the default `allow_empty_first_page=True`, an empty list has one empty page, so `get_page()` always returns something. ## What ListView does `ListView` (through `MultipleObjectMixin.paginate_queryset()`) is deliberately strict: - it reads the number from the URL keyword argument named by `page_kwarg` (default `page`), falling back to the query string; - the literal value `last` is accepted and mapped to the last page; - any other non-integer raises `Http404`; - it calls `paginator.page()` and converts any `InvalidPage` into `Http404`. So `/audit/?page=999` on a class-based list is a 404, while the same URL on a function view that uses `get_page()` shows the final page. Neither is wrong; it is a product decision. ## Testing the edges Both lookups are easy to cover with a few requests against the view: a valid page, a non-numeric value, zero, a number past the end, and an empty list. Asserting the status code and the page number shown for each case documents the chosen policy and stops a later refactor, such as moving a function view to `ListView`, from silently switching between clamping and 404s. ## Choosing - **Human-facing HTML list:** `get_page()`. A shared link to page 12 of a log that has since shrunk still shows the end of the log instead of an error page. - **Anything machine-read, or pages that should not be indexed:** `page()` with an explicit error. Returning the last page for every out-of-range number creates infinitely many URLs with the same content. - **Consistency across the site:** if most lists are `ListView`, their 404 behaviour is the norm; match it in function views with `page()` and `Http404`, or override `paginate_queryset()` in the class-based views to clamp instead.

  • Can Paginator.get_page() ever raise?
    Yes, in one case: the list is empty and the paginator was built with `allow_empty_first_page=False`. `num_pages` is then 0, `get_page()` falls back to page 0, and `page(0)` raises `EmptyPage`. With the default setting an empty list still has one empty page.
  • What does a Django ListView do with ?page=last?
    It maps the literal `last` to `paginator.num_pages` and shows the final page. Any other non-integer value raises `Http404`, and an out-of-range integer also becomes `Http404`, because `ListView` uses the strict `page()` lookup.

saying these in an interview costs you the question

  • get_page() and page() are aliases of the same method
  • page() returns None for a page number out of range
  • ListView silently clamps ?page=999 to the last page
  • EmptyPage and PageNotAnInteger are unrelated exception classes
  • get_page() can never raise any exception