Keyset Pagination for Django
Django's built-in pagination uses LIMIT/OFFSET. That works well for small offsets, but it gets progressively more expensive as the offset grows because the database still has to walk past the earlier rows.
This package implements keyset pagination, also called the seek method. Instead of asking for "page 200", the client asks for "the next page after this row" or "the previous page before this row". That keeps pagination efficient for large result sets, but it also means random page access and page-number iteration are not supported.
If you are new to the approach, the background articles from Markus Winand and Joe Nelson are worth reading.
Installation
Install the published package:
python -m pip install django-keyset-pagination-plus
The import path remains keyset_pagination.
Usage
In most class-based views you will want both the paginator and the included mixin, because Django's default pagination flow assumes integer page numbers.
from django.views.generic import ListView
from keyset_pagination.mixin import PaginateMixin
from keyset_pagination.paginator import KeysetPaginator
class EventList(PaginateMixin, ListView):
paginator_class = KeysetPaginator
paginate_by = 10
queryset = Event.objects.order_by("-timestamp", "group", "pk")
If your queryset does not call .order_by(...), the paginator will also honor the model's Meta.ordering. That lets you rely on a model's default ordering without repeating it in every view:
class Event(models.Model):
timestamp = models.DateTimeField()
group = models.TextField()
class Meta:
ordering = ("-timestamp", "group", "pk")
paginator = KeysetPaginator(Event.objects.all(), 10)
A few important constraints:
- The queryset must have a deterministic ordering. If you do not provide
.order_by(...), the paginator falls back to the model'sMeta.ordering. - If you explicitly clear ordering with
.order_by(), the paginator raisesValueError. - Use a stable ordering. In practice that usually means appending a unique tiebreaker such as
pk. - Ordering across related fields such as
location__nameis supported, but you should usually pair that withselect_related()to avoid extra queries when page tokens are generated. - Only next/previous navigation is available.
count,num_pages, and page-number iteration are intentionally unavailable.
Template usage
Cursor values are opaque JSON tokens built from the queryset ordering columns. Treat them as implementation details and pass them back unchanged.
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">Previous page</a>
{% endif %}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next page</a>
{% endif %}
The same approach works well with GET forms when the list also has filters:
{% if page_obj.has_previous %}
<button form="target-form"
name="page"
value="{{ page_obj.previous_page_number }}"
type="submit">
← Previous page
</button>
{% endif %}
{% if page_obj.has_next %}
<button form="target-form"
name="page"
value="{{ page_obj.next_page_number }}"
type="submit">
Next page →
</button>
{% endif %}
Cursor serialization handles values that do not round-trip cleanly through plain JSON types, including Decimal, datetime, date, time, and PostgreSQL range values. Decimal cursor values are parsed with Decimal, so precision is preserved even when the incoming token contains a bare JSON number.
Supported versions
The package supports Python 3.10+ and Django >=4.2,<6.1.
Continuous integration currently exercises Django 4.2, 5.2, and 6.0 on SQLite, plus PostgreSQL and MySQL integration environments for those Django releases.
Development
python -m pip install -e ".[dev,test]"
pre-commit install
tox -e py312-django52-sqlite
tox -p auto
Helpful shortcuts are also available through make:
make lint
make test
make test-all
make package
For local MySQL runs on macOS with Homebrew, install mysql-client and pkgconf. The MySQL tox environments default MYSQLCLIENT_CFLAGS and MYSQLCLIENT_LDFLAGS to Homebrew's mysql-client paths, and you can override them with environment variables if your local setup differs.
See schinckel.net: Keyset Pagination in Django for a longer walkthrough of the approach.
Release files for django-keyset-pagination-plus 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_keyset_pagination_plus-1.0.0.tar.gz | 8.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_keyset_pagination_plus-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 18.2 kB
Release files / django_keyset_pagination_plus-1.0.0.tar.gz
| Download URL | django_keyset_pagination_plus-1.0.0.tar.gz |
|---|---|
| Size | 8.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0523ab77d82c39a630d068d1ac5b6f51736679b23329e463adbbef4a90e7c405
|
|
BLAKE2b-256 checksum How to use checksums |
8b8b06fa8b4f294e9a3e2be7bd142b6a3e12851a4b0ecbc2465f17a2a06d1d74
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 27, 2026.
Transparency logRelease files / django_keyset_pagination_plus-1.0.0-py3-none-any.whl
| Download URL | django_keyset_pagination_plus-1.0.0-py3-none-any.whl |
|---|---|
| Size | 9.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
be26d181a113473212060454d27149ebf5de7ae5e8b91e2c23deb656c800d776
|
|
BLAKE2b-256 checksum How to use checksums |
e2568910c825a54de23cc5312e46670882b5a74a9a4e48c432a6df870fdf5eae
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 27, 2026.
Transparency log