skip to content

Pagination Styles

PageNumberPagination, LimitOffsetPagination and CursorPagination page list views via DEFAULT_PAGINATION_CLASS and PAGE_SIZE. Interviewers ask why cursor pagination needs a stable ordering.

part ofDjango REST Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In Django REST Framework, how do you enable pagination for list endpoints, and what does PageNumberPagination put in the response?

level: juniorimportance: must knowfreq 58%

answer

  1. off by default, two settings
  2. one without the other does nothing
  3. ?page=N and a special word
  4. four keys wrap the results

basics

~10 s

DRF paginates nothing by default: set DEFAULT_PAGINATION_CLASS and PAGE_SIZE, or pagination_class on a view. PageNumberPagination reads ?page=N and returns count, next, previous and results; a page beyond the end is a 404.

solid answer

~40 s

Both `DEFAULT_PAGINATION_CLASS` and `PAGE_SIZE` default to `None`, so list endpoints return a bare JSON array until you set both, for example `"rest_framework.pagination.PageNumberPagination"` with `PAGE_SIZE = 50`; setting only `PAGE_SIZE` triggers DRF's system check warning `rest_framework.W001`, and a class with no page size simply doesn't paginate. A generic view can override with `pagination_class` (or `None` to switch it off). `PageNumberPagination` reads `?page=N`, accepts `?page=last`, and wraps the page as `{"count", "next", "previous", "results"}` with absolute URLs for the links. A page number past the end, or a non-number, raises `NotFound` (404, "Invalid page."). Pagination is applied automatically only by `ListModelMixin.list()` in generic views and viewsets; a plain `APIView` must call the paginator itself.

code

python · 17 lines
python
from rest_framework import generics
from rest_framework.pagination import PageNumberPagination

from .models import Activity
from .serializers import ActivitySerializer


class ActivityPagination(PageNumberPagination):
    page_size = 25
    page_size_query_param = "page_size"
    max_page_size = 100


class ActivityList(generics.ListAPIView):
    queryset = Activity.objects.order_by("-created", "-id")
    serializer_class = ActivitySerializer
    pagination_class = ActivityPagination

go deeper

for a junior

Recall the two settings, that both default to None, and the count, next, previous and results keys.

for a middle

Explain how ListModelMixin.list() calls the paginator, why APIView does not paginate, and how invalid pages become 404.

for a senior

Make ordering deterministic, bound client page sizes, and decide which endpoints need a different style from the project default.

for a principal

Set one pagination envelope as part of the API contract and govern when an endpoint may deviate from it.

## Pagination is off until two settings agree Django REST Framework (DRF) splits pagination across two keys of the `REST_FRAMEWORK` settings dictionary, and **both default to `None`**: - `DEFAULT_PAGINATION_CLASS` — which **pagination class** generic list views use; - `PAGE_SIZE` — how many items a page holds, read by the built-in classes as their default page size. ```python # settings.py REST_FRAMEWORK = { "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 50, } ``` The two fail differently when only one is set: | Configured | Result | |---|---| | neither | list endpoints return a plain JSON array of every row | | `PAGE_SIZE` only | still unpaginated; `manage.py check` reports warning `rest_framework.W001` | | class only | `PageNumberPagination` has no page size, so `paginate_queryset()` returns `None` and the list is unpaginated | | both | every generic list view is paginated | ## Where pagination actually happens Pagination is wired into **generic views**, not into `APIView`: 1. `GenericAPIView.pagination_class` defaults to `DEFAULT_PAGINATION_CLASS`. 2. `ListModelMixin.list()` filters the queryset and calls `self.paginate_queryset(queryset)`. 3. If that returns a page, it serialises the page with `many=True` and returns `self.get_paginated_response(serializer.data)`. 4. If it returns `None` (no class, or no page size), it serialises the whole queryset. So `ListAPIView`, `ListCreateAPIView` and `ModelViewSet.list` paginate automatically; detail actions never do, and a hand-written `APIView` must instantiate a paginator itself. Setting `pagination_class = None` on one view opts it out. ## What `PageNumberPagination` reads and returns - Query parameter `page` (`page_query_param`), defaulting to page 1. - The word `last` (from `last_page_strings`) jumps to the final page. - `page_size_query_param` is `None` by default, so clients **cannot** change the page size unless you name a parameter such as `page_size` in a subclass — and then you should also set `max_page_size`. The response body for `GET /api/activity/?page=3` looks like: ```json { "count": 1023, "next": "https://api.example.org/api/activity/?page=4", "previous": "https://api.example.org/api/activity/?page=2", "results": ["..."] } ``` `next` and `previous` are absolute URLs built from the current request, or `null` at either end. `count` is the total across all pages, which costs a count query on every request. ## Invalid pages Under the hood `PageNumberPagination` uses Django's `Paginator` (its `django_paginator_class`). When `Paginator.page()` rejects the number — beyond the last page, zero, or not an integer — DRF raises `NotFound`, so the client gets **404** with a detail beginning "Invalid page.". This differs from `LimitOffsetPagination`, which answers an offset beyond the end with 200 and an empty `results` list. ## Ordering matters Page boundaries are only stable if the queryset has a deterministic order. An unordered queryset makes Django's paginator emit an `UnorderedObjectListWarning`, and rows can repeat or vanish between pages. Give the model a `Meta.ordering` or call `order_by()` with a unique tiebreaker such as `"-created", "-id"`. ## Choosing the class per view - Keep one project default so clients see a consistent envelope. - Override `pagination_class` per view when an endpoint needs a different style — for example `CursorPagination` for an infinite-scroll activity feed. - Subclass a built-in to change `page_size`, parameter names or the maximum page size, rather than rewriting pagination logic.

  • In DRF, what happens to ?page_size=0 when page_size_query_param is enabled?
    `get_page_size()` parses the value in strict mode, where zero counts as invalid, so it falls back to the class's `page_size`. A negative or non-numeric value falls back the same way. A value above `max_page_size` is silently clamped to the maximum rather than rejected.
  • Why does a DRF APIView returning Activity.objects.all() never paginate, even with DEFAULT_PAGINATION_CLASS set?
    The setting is read by `GenericAPIView.pagination_class`, and only `ListModelMixin.list()` calls the paginator. A plain `APIView` has neither, so it serialises whatever you return. Either switch to `ListAPIView` or instantiate the pagination class and call `paginate_queryset()` and `get_paginated_response()` yourself.

saying these in an interview costs you the question

  • DRF paginates list endpoints by default with PageNumberPagination.
  • Setting PAGE_SIZE alone is enough to turn pagination on.
  • Clients can pass ?page_size= to any PageNumberPagination endpoint.
  • A page number past the end returns an empty results list.
  • Every APIView is paginated once DEFAULT_PAGINATION_CLASS is set.
open as a page

In Django REST Framework, why does CursorPagination need a stable, unique ordering, and how would you configure it for an infinite-scroll activity feed?

level: middleimportance: must knowfreq 50%

basics

~20 s

CursorPagination encodes the value of the first ordering field and fetches rows beyond it, so that field must be unchanging, indexed, non-null and nearly unique. For a feed, order by '-created' with a unique tiebreaker.

open as a page

In Django REST Framework, how do you change the paginated response envelope with get_paginated_response(), and how do you paginate inside a plain APIView?

level: middleimportance: should knowfreq 36%

basics

~10 s

Subclass a pagination class and override get_paginated_response(data) to return any Response, such as bare results with Link headers, plus get_paginated_response_schema() for OpenAPI. In an APIView, call paginate_queryset(), serialise the page, then call get_paginated_response().

open as a page

A DRF list endpoint using LimitOffsetPagination on a very large table is slow and answers ?limit=1000000 — what causes both problems, and how do you harden it?

level: seniorimportance: should knowfreq 40%

basics

~10 s

LimitOffsetPagination's max_limit defaults to None, so any limit is honoured, and every page runs queryset.count(). Set max_limit, override get_count() or switch to CursorPagination for deep or unbounded lists.

open as a page