Keyset pagination (seek method) for Django.
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_keyset_pagination_plus-1.0.0.tar.gz.
File metadata
- Download URL: django_keyset_pagination_plus-1.0.0.tar.gz
- Upload date:
- Size: 8.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0523ab77d82c39a630d068d1ac5b6f51736679b23329e463adbbef4a90e7c405
|
|
| MD5 |
983bbb84d5a80f0c37b7cad2ab42a775
|
|
| BLAKE2b-256 |
8b8b06fa8b4f294e9a3e2be7bd142b6a3e12851a4b0ecbc2465f17a2a06d1d74
|
Provenance
The following attestation bundles were made for django_keyset_pagination_plus-1.0.0.tar.gz:
Publisher:
release.yml on schinckel/django-keyset-pagination
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_keyset_pagination_plus-1.0.0.tar.gz -
Subject digest:
0523ab77d82c39a630d068d1ac5b6f51736679b23329e463adbbef4a90e7c405 - Sigstore transparency entry: 1187241059
- Sigstore integration time:
-
Permalink:
schinckel/django-keyset-pagination@10bcf807b325bf1bc39dfd7b923c0f444ea60548 -
Branch / Tag:
refs/tags/1.0.0 - Owner: https://github.com/schinckel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@10bcf807b325bf1bc39dfd7b923c0f444ea60548 -
Trigger Event:
release
-
Statement type:
File details
Details for the file django_keyset_pagination_plus-1.0.0-py3-none-any.whl.
File metadata
- Download URL: django_keyset_pagination_plus-1.0.0-py3-none-any.whl
- Upload date:
- Size: 9.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be26d181a113473212060454d27149ebf5de7ae5e8b91e2c23deb656c800d776
|
|
| MD5 |
0b033a74fca70a132909c1988c10c865
|
|
| BLAKE2b-256 |
e2568910c825a54de23cc5312e46670882b5a74a9a4e48c432a6df870fdf5eae
|
Provenance
The following attestation bundles were made for django_keyset_pagination_plus-1.0.0-py3-none-any.whl:
Publisher:
release.yml on schinckel/django-keyset-pagination
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_keyset_pagination_plus-1.0.0-py3-none-any.whl -
Subject digest:
be26d181a113473212060454d27149ebf5de7ae5e8b91e2c23deb656c800d776 - Sigstore transparency entry: 1187241202
- Sigstore integration time:
-
Permalink:
schinckel/django-keyset-pagination@10bcf807b325bf1bc39dfd7b923c0f444ea60548 -
Branch / Tag:
refs/tags/1.0.0 - Owner: https://github.com/schinckel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@10bcf807b325bf1bc39dfd7b923c0f444ea60548 -
Trigger Event:
release
-
Statement type: