Skip to main content

Universal pagination toolkit for Python — one function, any backend, auto-detects sync/async

Project description

pypaginate

Fast, typed pagination, filtering, sorting & search for Python — one Rust core, byte-for-byte parity with the TypeScript package.

CI PyPI version Python Versions License: MIT codecov Ruff uv

pypaginate paginates, filters, sorts, and searches your data — in memory or against a database — through a single paginate() entry point and a small set of ergonomic helpers. The algorithms live once in a native Rust core (paginate-core) that ships inside the wheel, so there is no Rust toolchain to install and the Python and TypeScript (@cyblow/paginate) packages return identical results and byte-identical cursors.

📖 Full documentation: cyblow.github.io/paginate

Highlights

  • One paginate() for in-memory lists; ORM-backed pages via the adapters.
  • Filtering — 20 operators (eq, gte, contains, between, in, regex, …) with flat AND/OR lists and nested And() / Or() groups.
  • Sorting — stable, multi-key, with per-key direction and null placement.
  • Search — ranked full-text with optional trigram fuzzy matching and per-field weights — all in the native engine, no extra dependency.
  • Cursor (keyset) pagination — opaque, URL-safe cursors that decode byte-for-byte across Python, TypeScript, and Rust.
  • Integrations — SQLAlchemy 2.0 (sync + async), Django, and FastAPI.
  • Zero runtime dependencies in the core; Pydantic is optional (only the FastAPI extra needs it).
  • Fully typed (PEP 561) — the spec/param shapes are generated from the core's JSON Schema, so Python and TypeScript can't drift.

Installation

pip install pypaginate                  # core: in-memory + native engine
pip install "pypaginate[sqlalchemy]"    # SQLAlchemy 2.0 offset + keyset adapter
pip install "pypaginate[fastapi]"       # FastAPI dependencies (+ Pydantic)
pip install "pypaginate[django]"        # Django Q-object builders
pip install "pypaginate[all]"           # everything

Or with uv: uv add pypaginate (Python 3.11+). The native engine is bundled in the wheel; fuzzy/ranked search needs no extra.

Quick start

Paginate an in-memory list in three lines:

from pypaginate import paginate, OffsetParams

page = paginate([1, 2, 3, 4, 5], OffsetParams(page=1, limit=2))

page.items       # [1, 2]
page.total       # 5
page.pages       # 3
page.has_next    # True
list(page)       # [1, 2]  — OffsetPage is iterable, indexable, sized

Filter, sort, and search

The one-shot helpers run a single operation over any list of dicts or objects:

from pypaginate import filter, sort, search, FilterSpec, SortSpec, SearchSpec, And

users = [...]  # list of dicts or objects

adults = filter(users, FilterSpec(field="age", operator="gte", value=18))
grouped = filter(users, And(
    FilterSpec(field="age", operator="gte", value=18),
    FilterSpec(field="role", operator="in", value=["admin", "owner"]),
))
newest = sort(users, SortSpec(field="created_at", direction="desc"))
hits   = search(users, SearchSpec(query="alice", fields=["name", "email"]))

Operators, directions, and search modes are plain strings ("gte", "desc", "contains") — defined once in the core, identical in every language.

Query the same data repeatedly — Dataset

Each one-shot call marshals the whole collection into the engine. When you query the same rows many times, build a Dataset once and reuse it — it runs the full filter → search → sort → paginate pipeline in a single native call:

from pypaginate import Dataset, OffsetParams, FilterSpec, SortSpec

ds = Dataset(users)                         # marshals once

page = ds.page(
    OffsetParams(page=1, limit=20),
    filters=[FilterSpec(field="age", operator="gte", value=18)],
    sorting=[SortSpec(field="created_at", direction="desc")],
)
page.total      # matches across all pages
list(page)      # the page's rows (your original objects, never copied)

SQLAlchemy

pip install "pypaginate[sqlalchemy]" — Pydantic-free pagination for SQLAlchemy 2.0, sync or async, offset or keyset.

from sqlalchemy import select
from pypaginate import OffsetParams, FilterSpec, SortSpec
from pypaginate.adapters.sqlalchemy import (
    SyncSQLAlchemyBackend, build_filter, build_order_by,
)

stmt = select(User)
where = build_filter(User, [FilterSpec(field="status", operator="eq", value="active")])
if where is not None:
    stmt = stmt.where(where)
stmt = stmt.order_by(*build_order_by(User, [SortSpec(field="created_at", direction="desc")]))

page = SyncSQLAlchemyBackend(session).paginate(stmt, OffsetParams(page=1, limit=20))
page.items       # list[User]
page.total       # int
page.has_next    # bool

The async backend is identical with await:

from pypaginate.adapters.sqlalchemy import SQLAlchemyBackend

page = await SQLAlchemyBackend(async_session).paginate(stmt, OffsetParams(page=1, limit=20))

Cursor (keyset) pagination

For large or frequently-changing datasets where OFFSET is slow or unstable:

from sqlalchemy import select
from pypaginate import CursorParams
from pypaginate.adapters.sqlalchemy import SyncSQLAlchemyCursorBackend

stmt = select(Post).order_by(Post.created_at.desc(), Post.id.desc())
backend = SyncSQLAlchemyCursorBackend(session)

first = backend.fetch_page(stmt, CursorParams(limit=20))
nxt   = backend.fetch_page(stmt, CursorParams(limit=20, after=first.next_cursor))

first.items            # list[Post]
first.next_cursor      # str | None — feed back in as `after`
first.previous_cursor  # str | None
first.has_next         # bool

The cursor codec is shared with the TS and Django adapters, so a cursor minted by any of them decodes byte-for-byte in the others.

FastAPI

pip install "pypaginate[fastapi]"Annotated dependencies that turn a request's query string into pypaginate specs (invalid params become HTTP 422):

from fastapi import FastAPI
from pypaginate import paginate, OffsetPage
from pypaginate.adapters.fastapi import OffsetDep, SortDep, SearchDep

app = FastAPI()

@app.get("/users")
def list_users(params: OffsetDep, sort: SortDep, search: SearchDep) -> OffsetPage[dict]:
    # params: OffsetParams from ?page=&limit=   (defaults page=1, limit=20)
    # sort:   list[SortSpec] from ?sort=name,-age   ('-' prefix = descending)
    # search: SearchSpec | None from ?q=&search_fields=name,email
    users = [{"name": "Alice"}, {"name": "Bob"}]
    return paginate(users, params)
Dependency Query params Produces
OffsetDep ?page=1&limit=20 OffsetParams
CursorDep ?limit=20&after=… CursorParams
SortDep ?sort=name,-age list[SortSpec]
SearchDep ?q=alice&search_fields=name,email SearchSpec | None
FilterDep (declarative, user-defined) list[FilterSpec]

Declarative filters map query params onto FilterSpec conditions:

from typing import Annotated
from fastapi import Query
from pypaginate.adapters.fastapi import FilterDep, FilterField

class UserFilters(FilterDep):
    name: str | None = FilterField(None, operator="contains")
    min_age: int | None = FilterField(None, field="age", operator="gte")
    status: str | None = FilterField(None, operator="eq")

@app.get("/users")
def list_users(filters: Annotated[UserFilters, Query()]):
    specs = filters.to_specs()   # list[FilterSpec] for the non-None fields
    ...

Django

pip install "pypaginate[django]" — translate specs into Q objects and order_by arguments, then offset- or keyset-paginate a QuerySet (Django is imported lazily, so importing the adapter doesn't require Django):

from pypaginate import OffsetParams, FilterSpec, SortSpec
from pypaginate.adapters.django import apply_filters, apply_sorting, paginate_offset

qs = apply_filters(User.objects.all(), [FilterSpec(field="age", operator="gte", value=18)])
qs = apply_sorting(qs, [SortSpec(field="age", direction="desc")])

page = paginate_offset(qs, OffsetParams(page=1, limit=20))
page.total       # int
list(page)       # the page's model instances

paginate_keyset(qs, CursorParams(...)) provides cursor pagination over an explicitly ordered QuerySet.

Filter operators

Operator Meaning Operator Meaning
eq / ne Equal / not equal like / ilike SQL LIKE (case-(in)sensitive)
gt / gte Greater (or equal) between Inclusive range [lo, hi]
lt / lte Less (or equal) is_null / is_not_null Null / present
in / not_in Membership in a list empty / not_empty Empty / non-empty
contains Substring regex Regular-expression match
starts_with / ends_with Prefix / suffix exists Field path resolves

See the filtering guide for per-operator examples, and boolean groups for nested And() / Or() trees.

Errors

All failures derive from PaginateError (aliased PaginationError): ValidationError (bad page/limit) and its subclass InvalidCursorError (a malformed cursor), FilterError / FilterValidationError, SortError, SearchError / SearchQueryError, and ConfigurationError (unknown ORM field). Each carries a structured details mapping, mirrored by the TypeScript package.

Cross-language parity

Because the engine has a single implementation, pypaginate and @cyblow/paginate produce the same filtered/sorted/ranked order and byte-identical cursors. A frozen golden fixture is asserted by the Rust, Python, and TypeScript test suites in CI. See cross-language parity.

Development

git clone https://github.com/CybLow/paginate.git
cd paginate/py
uv sync                        # builds the native core (needs a Rust toolchain)

uv run pytest                  # tests
uv run pytest --cov            # coverage
uv run ruff format .           # format
uv run ruff check --fix .      # lint

See CONTRIBUTING.md.

Links

License

MIT — see LICENSE.

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

pypaginate-1.0.0.tar.gz (106.6 kB view details)

Uploaded Source

Built Distributions

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

pypaginate-1.0.0-cp311-abi3-win_arm64.whl (1.0 MB view details)

Uploaded CPython 3.11+Windows ARM64

pypaginate-1.0.0-cp311-abi3-win_amd64.whl (1.1 MB view details)

Uploaded CPython 3.11+Windows x86-64

pypaginate-1.0.0-cp311-abi3-musllinux_1_2_x86_64.whl (1.4 MB view details)

Uploaded CPython 3.11+musllinux: musl 1.2+ x86-64

pypaginate-1.0.0-cp311-abi3-musllinux_1_2_aarch64.whl (1.3 MB view details)

Uploaded CPython 3.11+musllinux: musl 1.2+ ARM64

pypaginate-1.0.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.1 MB view details)

Uploaded CPython 3.11+manylinux: glibc 2.17+ x86-64

pypaginate-1.0.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.1 MB view details)

Uploaded CPython 3.11+manylinux: glibc 2.17+ ARM64

pypaginate-1.0.0-cp311-abi3-macosx_10_12_x86_64.whl (1.1 MB view details)

Uploaded CPython 3.11+macOS 10.12+ x86-64

File details

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

File metadata

  • Download URL: pypaginate-1.0.0.tar.gz
  • Upload date:
  • Size: 106.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pypaginate-1.0.0.tar.gz
Algorithm Hash digest
SHA256 b9a9613c9a367a95c7f191cd6461bdb1568d702db3189c69063321ef8b147961
MD5 f601f7050277863e1eafd3b59267e29b
BLAKE2b-256 9cc1f7f5bbe474dcbd38b1b0d5cbba26716ad56c5b066d70c98254b0222b9ed2

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-1.0.0.tar.gz:

Publisher: release-python.yml on CybLow/paginate

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

File details

Details for the file pypaginate-1.0.0-cp311-abi3-win_arm64.whl.

File metadata

  • Download URL: pypaginate-1.0.0-cp311-abi3-win_arm64.whl
  • Upload date:
  • Size: 1.0 MB
  • Tags: CPython 3.11+, Windows ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pypaginate-1.0.0-cp311-abi3-win_arm64.whl
Algorithm Hash digest
SHA256 a105ab42011fd3df2fbd32fcca831bd0aa887b02b840fbfab9b91003a1e421de
MD5 275669de8e9136104abc79ae3176357c
BLAKE2b-256 c2c3af6e1409718a6290d7caa6aafcad16e60b29e95722447eb315ba8a94554b

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-1.0.0-cp311-abi3-win_arm64.whl:

Publisher: release-python.yml on CybLow/paginate

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

File details

Details for the file pypaginate-1.0.0-cp311-abi3-win_amd64.whl.

File metadata

  • Download URL: pypaginate-1.0.0-cp311-abi3-win_amd64.whl
  • Upload date:
  • Size: 1.1 MB
  • Tags: CPython 3.11+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pypaginate-1.0.0-cp311-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 43bad14811d3620b0c7253ef9590beeca4613850d97a680ad5f0ab44e2406bb4
MD5 53064298b1896473cfa12105aee75f3c
BLAKE2b-256 8c4e23bacdcaaafafc2efaabfc0a81033a5d5c19fe67cb4c85f92d7e09742cb8

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-1.0.0-cp311-abi3-win_amd64.whl:

Publisher: release-python.yml on CybLow/paginate

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

File details

Details for the file pypaginate-1.0.0-cp311-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for pypaginate-1.0.0-cp311-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 49046f5695c96a08974cdeecaf054b2d04b59c585b55dbeaa9f86db5559572e7
MD5 7f197fb0e566ac7408297d17b103862a
BLAKE2b-256 97a8216bf9aa828582205b5e80bd3146e58e1ec369884b05360dc103a480ee10

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-1.0.0-cp311-abi3-musllinux_1_2_x86_64.whl:

Publisher: release-python.yml on CybLow/paginate

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

File details

Details for the file pypaginate-1.0.0-cp311-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for pypaginate-1.0.0-cp311-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 830c069e70c00605f777344717d9ddb241dbc45ab28d474638288e894540026f
MD5 23047aa3bf37b418080fb902c83457f6
BLAKE2b-256 5b0ddad9b624214778eaaf9f33f527de9e4cd9f85d35ede701ea3aff749bd255

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-1.0.0-cp311-abi3-musllinux_1_2_aarch64.whl:

Publisher: release-python.yml on CybLow/paginate

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

File details

Details for the file pypaginate-1.0.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for pypaginate-1.0.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 a96e5afeab70a7058898c92475617c18f77949489fcf04e003da4a474b74554c
MD5 891f85d7e96fe5af02208556d5655d70
BLAKE2b-256 c27ff538689c7c83e8c0fc6151ec6f425360fca7b2ec5f10688785d4701893e4

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-1.0.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: release-python.yml on CybLow/paginate

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

File details

Details for the file pypaginate-1.0.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for pypaginate-1.0.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 bf89295ff3f1c9d67106b38f1f0c5e943f575bae9ae8b255754407d561bd3752
MD5 cba3eed70813853001cb170167f859ce
BLAKE2b-256 ee3a69149d919a9796113e5deff105c6a5635d814a42dd0e1b740a9b7323479b

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-1.0.0-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: release-python.yml on CybLow/paginate

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

File details

Details for the file pypaginate-1.0.0-cp311-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for pypaginate-1.0.0-cp311-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 7b5437f8df976ed284744d108201b7f7c514b27e334f80f3693273426b51ffef
MD5 674f5904c2d56b1bac98ba24d5e88abc
BLAKE2b-256 51ebd53a3416384e04cb3ec5d04992a441f3ea4e3fd0ae054a4654c5c6debbd2

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-1.0.0-cp311-abi3-macosx_10_12_x86_64.whl:

Publisher: release-python.yml on CybLow/paginate

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