Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/api-guide/pagination.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ Pagination can be turned off by setting the pagination class to `None`.

## Setting the pagination style

The pagination style may be set globally, using the `DEFAULT_PAGINATION_CLASS` and `PAGE_SIZE` setting keys. For example, to use the built-in limit/offset pagination, you would do something like this:
The pagination style may be set globally, using the `DEFAULT_PAGINATION_CLASS`, `PAGE_SIZE` and `MAX_PAGE_SIZE` setting keys. For example, to use the built-in limit/offset pagination, you would do something like this:

REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 100
'PAGE_SIZE': 100,
'MAX_PAGE_SIZE': 250,
}

Note that you need to set both the pagination class, and the page size that should be used. Both `DEFAULT_PAGINATION_CLASS` and `PAGE_SIZE` are `None` by default.
Note that you need to set both the pagination class, and the page size and limit that should be used.
`DEFAULT_PAGINATION_CLASS`, `PAGE_SIZE`, `MAX_PAGE_SIZE` are `None` by default.
Comment on lines +35 to +36
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This note is a bit too strong/inaccurate: you don’t always need to set both PAGE_SIZE and MAX_PAGE_SIZE for pagination to work (e.g., LimitOffsetPagination can rely on the client-provided limit, and MAX_PAGE_SIZE only matters when a client-controllable size param is enabled). Consider rewording to clarify which settings are required vs optional depending on the pagination class.

Copilot uses AI. Check for mistakes.

You can also set the pagination class on an individual view by using the `pagination_class` attribute. Typically you'll want to use the same pagination style throughout your API, although you might want to vary individual aspects of the pagination, such as default or maximum page size, on a per-view basis.

Expand Down
8 changes: 5 additions & 3 deletions rest_framework/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ def pagination_system_check(app_configs, **kwargs):
errors = []
# Use of default page size setting requires a default Paginator class
from rest_framework.settings import api_settings
if api_settings.PAGE_SIZE and not api_settings.DEFAULT_PAGINATION_CLASS:
if (
api_settings.PAGE_SIZE or api_settings.MAX_PAGE_SIZE
) and not api_settings.DEFAULT_PAGINATION_CLASS:
errors.append(
Warning(
"You have specified a default PAGE_SIZE pagination rest_framework setting, "
"You have specified a default PAGE_SIZE pagination or MAX_PAGE_SIZE limit rest_framework setting, "
"without specifying also a DEFAULT_PAGINATION_CLASS.",
hint="The default for DEFAULT_PAGINATION_CLASS is None. "
"In previous versions this was PageNumberPagination. "
"If you wish to define PAGE_SIZE globally whilst defining "
"If you wish to define PAGE_SIZE or MAX_PAGE_SIZE globally whilst defining "
"pagination_class on a per-view basis you may silence this check.",
id="rest_framework.W001"
)
Expand Down
77 changes: 63 additions & 14 deletions rest_framework/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,6 @@ class PageNumberPagination(BasePagination):
http://api.example.org/accounts/?page=4
http://api.example.org/accounts/?page=4&page_size=100
"""
# The default page size.
# Defaults to `None`, meaning pagination is disabled.
page_size = api_settings.PAGE_SIZE

django_paginator_class = DjangoPaginator

Expand All @@ -175,16 +172,33 @@ class PageNumberPagination(BasePagination):
page_size_query_param = None
page_size_query_description = _('Number of results to return per page.')

# Set to an integer to limit the maximum page size the client may request.
# Only relevant if 'page_size_query_param' has also been set.
max_page_size = None

last_page_strings = ('last',)

template = 'rest_framework/pagination/numbers.html'

invalid_page_message = _('Invalid page.')

@property
def page_size(self) -> int:
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type annotation here should allow None. api_settings.PAGE_SIZE defaults to None (pagination disabled), so annotating this as int is inaccurate and can confuse type checkers; consider using Optional[int]/int | None (or dropping the annotation to match the rest of the module).

Copilot uses AI. Check for mistakes.
"""Get default page size.

Defaults to `None`, meaning pagination is disabled.

"""
Comment on lines +182 to +187
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There shoud probably be a comment explaining why this i a property to avoid removing it in a future refactor?

return api_settings.PAGE_SIZE

@property
def max_page_size(self) -> int:
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same type issue as page_size: api_settings.MAX_PAGE_SIZE defaults to None, so this property's return type should be Optional[int]/int | None rather than int (or omit the annotation for consistency).

Copilot uses AI. Check for mistakes.
"""Limit page size.

Set to an integer to limit the maximum page size the client may request.
Only relevant if 'page_size_query_param' has also been set.
Defaults to `None`, meaning page size is unlimited.
It's recommended that you would set a limit to avoid api abuse.
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring grammar/casing: “It's recommended that you would…” is ungrammatical, and “api” should be “API”. Consider rephrasing to “It's recommended that you set a limit to avoid API abuse.”

Suggested change
It's recommended that you would set a limit to avoid api abuse.
It's recommended that you set a limit to avoid API abuse.

Copilot uses AI. Check for mistakes.

"""
return api_settings.MAX_PAGE_SIZE

def paginate_queryset(self, queryset, request, view=None):
"""
Paginate a queryset if required, either returning a
Expand Down Expand Up @@ -338,14 +352,33 @@ class LimitOffsetPagination(BasePagination):
http://api.example.org/accounts/?limit=100
http://api.example.org/accounts/?offset=400&limit=100
"""
default_limit = api_settings.PAGE_SIZE
limit_query_param = 'limit'
limit_query_description = _('Number of results to return per page.')
offset_query_param = 'offset'
offset_query_description = _('The initial index from which to return the results.')
max_limit = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid changing the style here?

max_limit = api_settings.MAX_PAGE_SIZE

Perfer minimal impact footprints for PRs.

Copy link
Copy Markdown
Contributor Author

@TheSuperiorStanislav TheSuperiorStanislav Sep 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's related to #8993 and #9107 (comment). Without it we can't properly test it, since this setting will be set during init.

Copy link
Copy Markdown
Contributor

@lovelydinosaur lovelydinosaur Sep 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrm... not sure? If you'd like to take that approach then I'd probably suggest handling #8993 separately, as a precursor to this.
Unclear to me why that'd be different to attributes we use in other cases.

template = 'rest_framework/pagination/numbers.html'

@property
def max_limit(self) -> int:
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same typing concern as elsewhere: api_settings.MAX_PAGE_SIZE (and PAGE_SIZE) default to None, so annotating these as int is inaccurate. Adjust the annotations to allow None or omit them for consistency with the rest of the module.

Copilot uses AI. Check for mistakes.
"""Limit maximum page size.

Set to an integer to limit the maximum page size the client may request.
Only relevant if 'page_size_query_param' has also been set.
Defaults to `None`, meaning page size is unlimited.
It's recommended that you would set a limit to avoid api abuse.
Comment on lines +365 to +368
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LimitOffsetPagination uses limit_query_param, not page_size_query_param, so this docstring is misleading. Also the same grammar/casing issue applies (“API abuse”). Update the text to refer to limit_query_param (and/or max_limit) and fix the wording.

Suggested change
Set to an integer to limit the maximum page size the client may request.
Only relevant if 'page_size_query_param' has also been set.
Defaults to `None`, meaning page size is unlimited.
It's recommended that you would set a limit to avoid api abuse.
Set to an integer to limit the maximum number of results the client may
request via the ``limit_query_param``.
Defaults to ``None``, meaning the limit is unrestricted.
It is recommended that you set a limit to avoid API abuse.

Copilot uses AI. Check for mistakes.

"""
return api_settings.MAX_PAGE_SIZE
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_PAGE_SIZE is now wired into LimitOffsetPagination via max_limit (and also into CursorPagination/PageNumberPagination). There are new integration tests for PageNumberPagination settings, but no corresponding tests that MAX_PAGE_SIZE actually caps limit for LimitOffsetPagination (and/or caps cursor page size when enabled). Adding a couple of targeted tests would help prevent regressions across pagination styles.

Copilot uses AI. Check for mistakes.

@property
def default_limit(self) -> int:
"""Get default page size.

Defaults to `None`, meaning pagination is disabled.

"""
return api_settings.PAGE_SIZE

def paginate_queryset(self, queryset, request, view=None):
self.request = request
self.limit = self.get_limit(request)
Expand Down Expand Up @@ -523,7 +556,6 @@ class CursorPagination(BasePagination):
"""
cursor_query_param = 'cursor'
cursor_query_description = _('The pagination cursor value.')
page_size = api_settings.PAGE_SIZE
invalid_cursor_message = _('Invalid cursor')
ordering = '-created'
template = 'rest_framework/pagination/previous_and_next.html'
Expand All @@ -533,16 +565,33 @@ class CursorPagination(BasePagination):
page_size_query_param = None
page_size_query_description = _('Number of results to return per page.')

# Set to an integer to limit the maximum page size the client may request.
# Only relevant if 'page_size_query_param' has also been set.
max_page_size = None

# The offset in the cursor is used in situations where we have a
# nearly-unique index. (Eg millisecond precision creation timestamps)
# We guard against malicious users attempting to cause expensive database
# queries, by having a hard cap on the maximum possible size of the offset.
offset_cutoff = 1000

@property
def page_size(self) -> int:
"""Get default page size.

Defaults to `None`, meaning pagination is disabled.

"""
return api_settings.PAGE_SIZE

@property
def max_page_size(self) -> int:
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These settings-backed properties can return None (MAX_PAGE_SIZE defaults to None), but the return annotation is int. Consider changing to Optional[int]/int | None (or removing the annotation) to avoid misleading type information.

Copilot uses AI. Check for mistakes.
"""Limit page size.

Set to an integer to limit the maximum page size the client may request.
Only relevant if 'page_size_query_param' has also been set.
Defaults to `None`, meaning page size is unlimited.
It's recommended that you would set a limit to avoid api abuse.
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same docstring grammar/casing issue as earlier (“It's recommended that you would…”, “api” → “API”). Please rephrase for correct English and consistent capitalization.

Suggested change
It's recommended that you would set a limit to avoid api abuse.
It is recommended to set a limit to avoid API abuse.

Copilot uses AI. Check for mistakes.

"""
return api_settings.MAX_PAGE_SIZE

def paginate_queryset(self, queryset, request, view=None):
self.request = request
self.page_size = self.get_page_size(request)
Comment on lines 595 to 597
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CursorPagination.page_size is now a read-only @Property, but paginate_queryset() assigns to self.page_size. That assignment will raise AttributeError at runtime. Consider either keeping page_size as a regular attribute, or adding a setter/backing attribute so paginate_queryset can cache the computed per-request page size safely.

Copilot uses AI. Check for mistakes.
Expand Down
1 change: 1 addition & 0 deletions rest_framework/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@

# Pagination
'PAGE_SIZE': None,
"MAX_PAGE_SIZE": None,
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Project settings dict uses single quotes for keys throughout this file; this introduces a double-quoted key. For consistency (and to minimize noisy diffs), use single quotes for 'MAX_PAGE_SIZE' here.

Suggested change
"MAX_PAGE_SIZE": None,
'MAX_PAGE_SIZE': None,

Copilot uses AI. Check for mistakes.

# Filtering
'SEARCH_PARAM': 'search',
Expand Down
73 changes: 72 additions & 1 deletion tests/test_pagination.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest
from django.core.paginator import Paginator as DjangoPaginator
from django.db import models
from django.test import TestCase
from django.test import TestCase, override_settings

from rest_framework import (
exceptions, filters, generics, pagination, serializers, status
Expand Down Expand Up @@ -135,6 +135,77 @@ def test_404_not_found_for_invalid_page(self):
}


class TestPaginationSettingsIntegration:
"""
Integration tests for pagination settings.
"""

def setup_method(self):
class PassThroughSerializer(serializers.BaseSerializer):
def to_representation(self, item):
return item

class EvenItemsOnly(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
return [item for item in queryset if item % 2 == 0]

class BasicPagination(pagination.PageNumberPagination):
page_size_query_param = 'page_size'

self.view = generics.ListAPIView.as_view(
serializer_class=PassThroughSerializer,
queryset=range(1, 101),
filter_backends=[EvenItemsOnly],
pagination_class=BasicPagination
)

@override_settings(
REST_FRAMEWORK={
"MAX_PAGE_SIZE": 20,
"PAGE_SIZE": 5,
}
)
Comment on lines +162 to +167
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test block switches to double quotes for strings (e.g., REST_FRAMEWORK keys and response.data indexing) while the surrounding tests in this module predominantly use single quotes. Consider using the existing quote style here for consistency and to reduce diff noise.

Copilot uses AI. Check for mistakes.
def test_setting_page_size_over_maximum(self):
"""
When page_size parameter exceeds maximum allowable,
then it should be capped to the maximum.
"""
request = factory.get('/', {'page_size': 1000})
response = self.view(request)
assert response.status_code == status.HTTP_200_OK
assert len(response.data["results"]) == 20, response.data
assert response.data == {
'results': [
2, 4, 6, 8, 10, 12, 14, 16, 18, 20,
22, 24, 26, 28, 30, 32, 34, 36, 38, 40
],
'previous': None,
'next': 'http://testserver/?page=2&page_size=1000',
'count': 50
}

@override_settings(
REST_FRAMEWORK={
"MAX_PAGE_SIZE": 20,
"PAGE_SIZE": 5,
}
)
def test_setting_page_size_to_zero(self):
"""
When page_size parameter is invalid it should return to the default.
"""
request = factory.get('/', {'page_size': 0})
response = self.view(request)
assert response.status_code == status.HTTP_200_OK
assert len(response.data["results"]) == 5, response.data
assert response.data == {
'results': [2, 4, 6, 8, 10],
'previous': None,
'next': 'http://testserver/?page=2&page_size=0',
'count': 50
}


class TestPaginationDisabledIntegration:
"""
Integration tests for disabled pagination.
Expand Down
9 changes: 9 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def get_pagination_error(error_id: str):
return next((error for error in errors if error.id == error_id), None)

self.assertIsNone(api_settings.PAGE_SIZE)
self.assertIsNone(api_settings.MAX_PAGE_SIZE)
self.assertIsNone(api_settings.DEFAULT_PAGINATION_CLASS)

pagination_error = get_pagination_error('rest_framework.W001')
Expand All @@ -63,11 +64,19 @@ def get_pagination_error(error_id: str):
pagination_error = get_pagination_error('rest_framework.W001')
self.assertIsNotNone(pagination_error)

with override_settings(REST_FRAMEWORK={'MAX_PAGE_SIZE': 10}):
pagination_error = get_pagination_error('rest_framework.W001')
self.assertIsNotNone(pagination_error)

default_pagination_class = 'rest_framework.pagination.PageNumberPagination'
with override_settings(REST_FRAMEWORK={'PAGE_SIZE': 10, 'DEFAULT_PAGINATION_CLASS': default_pagination_class}):
pagination_error = get_pagination_error('rest_framework.W001')
self.assertIsNone(pagination_error)

with override_settings(REST_FRAMEWORK={'MAX_PAGE_SIZE': 10, 'DEFAULT_PAGINATION_CLASS': default_pagination_class}):
pagination_error = get_pagination_error('rest_framework.W001')
self.assertIsNone(pagination_error)


class TestSettingTypes(TestCase):
def test_settings_consistently_coerced_to_list(self):
Expand Down
Loading