Skip to main content

Pagination

pypaginate covers both pagination models: offset (page/limit) for in-memory lists and bounded sets, and keyset (cursor) for large or frequently-changing database queries. It also offers a resident Dataset that fuses filter → sort → paginate into one native call.

Offset pagination

You build the input with OffsetParams, pass it to paginate, and get back an OffsetPage.

Parameters

OffsetParams holds a 1-based page (default 1) and a limit (default 20, range 1..=MAX_LIMIT). Both are validated at construction by the core — an out-of-range value raises ValidationError. The zero-based row offset is derived for you:

from pypaginate import OffsetParams, MAX_LIMIT

params = OffsetParams(page=2, limit=20)
params.offset # 20
MAX_LIMIT # the shared maximum page size (1000) — a DoS guard

Paginating a list

from pypaginate import paginate, OffsetParams

page = paginate(users, OffsetParams(page=1, limit=20))

page.total # total rows across all pages (int)
page.page # the requested page number
page.pages # total number of pages — ceil(total / limit)
page.limit # rows per page
page.has_next # bool
page.has_previous # bool

for user in page: # OffsetPage is iterable…
...
page[0] # …indexable…
len(page) # …and sized

Pages beyond the range

Requesting a page past the last one is not an error — you get empty items while the metadata still describes the full dataset:

users = list(range(50))
page = paginate(users, OffsetParams(page=100, limit=20))
list(page) # []
page.total # 50
page.pages # 3
page.has_next # False
page.has_previous # True

Beyond plain slicing

The one-shot paginate is pure offset slicing. To filter, sort, and search in the same native call, use a Dataset. To paginate a database query without loading every row, use the SQLAlchemy or Django integrations.

Keyset (cursor) pagination

Keyset pagination pages by the ordering values of the last row seen, so it stays correct and fast under writes — see pagination models. It is database-backed; there is no in-memory keyset helper (for a list you hold, offset is simpler and just as fast).

Parameters

CursorParams holds a limit plus at most one of after / before (mutually exclusive — after pages forward, before backward, neither for the first page):

from pypaginate import CursorParams

CursorParams(limit=20) # first page
CursorParams(limit=20, after=cursor) # page forward
CursorParams(limit=20, before=cursor) # page backward

The result page

A keyset query returns a CursorPage carrying the rows and the navigation cursors — but no total or page number:

FieldMeaning
itemsthe rows on this page
limitrequested page size
has_next / has_previousa following / preceding page exists
next_cursor / previous_cursorcursor to fetch the next / previous page, or None

Feed next_cursor back in as the next request's after, and previous_cursor as before. The cursor codec is shared across languages — see cursors.

Driving it

Keyset is driven by the ORM integrations, which read your query's ORDER BY, render the keyset predicate, over-fetch limit + 1, trim, and assemble the CursorPage:

The resident Dataset

The one-shot helpers marshal your whole collection into the core on every call. When you query the same rows repeatedly, a Dataset marshals once, then answers many queries natively (returning indices the wrapper maps back to your objects). Build once, query many.

from pypaginate import Dataset

ds = Dataset(users) # marshals once
len(ds) # number of rows held

Querying

filter, sort, and search each return a new list, using the same spec shapes as the one-shot helpers:

from pypaginate import Dataset, FilterSpec, SortSpec, SearchSpec

ds = Dataset(users)

adults = ds.filter([FilterSpec(field="age", operator="gte", value=18)])
newest = ds.sort([SortSpec(field="created_at", direction="desc")])
hits = ds.search(SearchSpec(query="alice", fields=["name", "email"]))

The whole pipeline in one call

page runs filter → search → sort → offset-paginate in a single native call, returning an OffsetPage. Each stage is optional:

page = ds.page(
OffsetParams(page=1, limit=20),
filters=[FilterSpec(field="age", operator="gte", value=18)],
sorting=[SortSpec(field="created_at", direction="desc")],
search=SearchSpec(query="alice", fields=["name", "email"]),
)
page.total # matches across all pages
list(page) # the page's rows

When to reach for it

Use…When
one-shot filter / sort / search / paginatea single query, or data that changes every call
Datasetseveral queries against the same rows — paging plus re-sorting, faceted filtering, repeated searches

A Dataset operates on rows you already hold in memory; for a database query, use the integrations. See performance for the measured speedups.

Next