Skip to main content

Keyset pagination (seek method) for Django.

Project description

Keyset Pagination for Django

CI

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's Meta.ordering.
  • If you explicitly clear ordering with .order_by(), the paginator raises ValueError.
  • Use a stable ordering. In practice that usually means appending a unique tiebreaker such as pk.
  • Ordering across related fields such as location__name is supported, but you should usually pair that with select_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">
    &larr; 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 &rarr;
  </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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_keyset_pagination_plus-1.0.0.tar.gz (8.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_keyset_pagination_plus-1.0.0-py3-none-any.whl (9.3 kB view details)

Uploaded Python 3

File details

Details for the file django_keyset_pagination_plus-1.0.0.tar.gz.

File metadata

File hashes

Hashes for django_keyset_pagination_plus-1.0.0.tar.gz
Algorithm Hash digest
SHA256 0523ab77d82c39a630d068d1ac5b6f51736679b23329e463adbbef4a90e7c405
MD5 983bbb84d5a80f0c37b7cad2ab42a775
BLAKE2b-256 8b8b06fa8b4f294e9a3e2be7bd142b6a3e12851a4b0ecbc2465f17a2a06d1d74

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_keyset_pagination_plus-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_keyset_pagination_plus-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 be26d181a113473212060454d27149ebf5de7ae5e8b91e2c23deb656c800d776
MD5 0b033a74fca70a132909c1988c10c865
BLAKE2b-256 e2568910c825a54de23cc5312e46670882b5a74a9a4e48c432a6df870fdf5eae

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page