Skip to main content

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

Project description

pypaginate

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

CI PyPI version Python Versions License: MIT codecov Ruff uv

pypaginate provides a single paginate() function that works with lists, SQLAlchemy queries (async and sync), and cursor-based pagination. The return type is automatically inferred from the params you pass in.

Features

  • One function -- paginate() handles lists, SQLAlchemy queries, sync and async
  • Type-safe inference -- OffsetParams returns OffsetPage, CursorParams returns CursorPage
  • Filtering -- 20 operators (eq, gte, contains, between, regex, etc.)
  • Sorting -- multi-column with direction and null placement control
  • Search -- full-text with optional fuzzy matching (RapidFuzz)
  • FastAPI -- Annotated dependencies for pagination, filtering, sorting, and search
  • Cursor pagination -- keyset/cursor-based pagination via sqlakeyset
  • Pipeline -- compose filter + sort + search + paginate in one call
  • 100% typed -- mypy strict mode, Pydantic v2 models

Installation

# Core (in-memory pagination only)
pip install pypaginate

# With SQLAlchemy support
pip install pypaginate[sqlalchemy]

# With FastAPI integration
pip install pypaginate[fastapi]

# With fuzzy search (RapidFuzz)
pip install pypaginate[search]

# Everything
pip install pypaginate[all]

Or with uv:

uv add pypaginate
uv add pypaginate[all]

Quick Start

Paginate a list in 3 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

SQLAlchemy (Async)

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from pypaginate import paginate, OffsetParams
from pypaginate.adapters.sqlalchemy import SQLAlchemyBackend

async def list_users(session: AsyncSession):
    stmt = select(User).order_by(User.created_at.desc())
    backend = SQLAlchemyBackend(session)

    page = await paginate(stmt, OffsetParams(page=1, limit=20), backend=backend)

    page.items       # list[User]
    page.total       # int
    page.has_next    # bool

For sync sessions, use SyncSQLAlchemyBackend:

from sqlalchemy.orm import Session
from pypaginate.adapters.sqlalchemy import SyncSQLAlchemyBackend

def list_users(session: Session):
    backend = SyncSQLAlchemyBackend(session)
    page = paginate(select(User), OffsetParams(page=1, limit=20), backend=backend)

Cursor Pagination

For large datasets where offset-based pagination is inefficient:

from pypaginate import paginate, CursorParams
from pypaginate.adapters.sqlalchemy import SQLAlchemyCursorBackend

async def scroll_users(session: AsyncSession, cursor: str | None = None):
    stmt = select(User).order_by(User.id)
    backend = SQLAlchemyCursorBackend(session)

    page = await paginate(stmt, CursorParams(limit=20, after=cursor), backend=backend)

    page.items            # list[User]
    page.next_cursor      # str | None -- pass to next request
    page.previous_cursor  # str | None
    page.has_next         # bool

FastAPI Integration

pypaginate provides Annotated dependency types for clean FastAPI integration:

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

app = FastAPI()

@app.get("/users")
async def list_users(params: OffsetDep) -> OffsetPage[dict]:
    users = [{"name": "Alice"}, {"name": "Bob"}, {"name": "Charlie"}]
    return paginate(users, params)

Available dependencies:

Dependency Query Params Produces
OffsetDep ?page=1&limit=20 OffsetParams
CursorDep ?limit=20&after=abc CursorParams
FilterDep (user-defined fields) list[FilterSpec]
SortDep ?sort=name,-age list[SortSpec]
SearchDep ?q=alice&search_fields=name,email SearchSpec

Declarative Filters

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")
    age_min: int | None = FilterField(None, field="age", operator="gte")
    status: str | None = FilterField(None, operator="eq")

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

Sorting and Search

from pypaginate.adapters.fastapi import OffsetDep, SortDep, SearchDep

@app.get("/users")
async def list_users(params: OffsetDep, sort: SortDep, search: SearchDep):
    # sort: ?sort=name,-created_at  (- prefix = descending)
    # search: ?q=alice&search_fields=name,email
    ...

Filtering

Use FilterSpec to define filter conditions:

from pypaginate import FilterSpec
from pypaginate.filtering import FilterEngine

engine = FilterEngine()

users = [
    {"name": "Alice", "age": 30, "status": "active"},
    {"name": "Bob", "age": 25, "status": "inactive"},
    {"name": "Charlie", "age": 35, "status": "active"},
]

# Simple equality
active = engine.apply(users, [FilterSpec(field="status", value="active")])
# [Alice, Charlie]

# Multiple filters (AND by default)
result = engine.apply(users, [
    FilterSpec(field="age", operator="gte", value=30),
    FilterSpec(field="status", value="active"),
])
# [Alice, Charlie]

Nested Filter Groups

from pypaginate import And, Or, FilterSpec

# (status = active) AND (age >= 30 OR name contains "bob")
group = And(
    FilterSpec(field="status", value="active"),
    Or(
        FilterSpec(field="age", operator="gte", value=30),
        FilterSpec(field="name", operator="contains", value="bob"),
    ),
)

result = engine.apply(users, group)

Available Filter Operators

Operator Description Example
eq, ne Equality / inequality FilterSpec(field="status", value="active")
gt, gte, lt, lte Comparisons FilterSpec(field="age", operator="gte", value=18)
in, not_in Membership FilterSpec(field="role", operator="in", value=["admin", "user"])
contains, starts_with, ends_with Text matching FilterSpec(field="name", operator="contains", value="ali")
like, ilike SQL-style patterns FilterSpec(field="email", operator="like", value="%@gmail.com")
between Range FilterSpec(field="price", operator="between", value=[10, 100])
is_null, is_not_null Null checks FilterSpec(field="notes", operator="is_null")
empty, not_empty Empty checks FilterSpec(field="tags", operator="not_empty")
exists Field existence FilterSpec(field="id", operator="exists")
regex Regex matching FilterSpec(field="code", operator="regex", value="^A\\d+")

Sorting

from pypaginate import SortSpec, SortDirection

from pypaginate.sorting import SortEngine

engine = SortEngine()

users = [
    {"name": "Charlie", "age": 35},
    {"name": "Alice", "age": 30},
    {"name": "Bob", "age": 25},
]

sorted_users = engine.apply(users, [
    SortSpec(field="age", direction=SortDirection.DESC),
])
# [Charlie (35), Alice (30), Bob (25)]

Search

from pypaginate import SearchSpec

from pypaginate.search import SearchEngine

engine = SearchEngine()

users = [
    {"name": "Alice Smith", "email": "alice@example.com"},
    {"name": "Bob Johnson", "email": "bob@example.com"},
]

results = engine.apply(users, SearchSpec(
    query="alice",
    fields=("name", "email"),
))
# [Alice Smith]

Fuzzy search (requires pypaginate[search]):

from pypaginate import SearchSpec, FuzzyMode

results = engine.apply(users, SearchSpec(
    query="alce",
    fields=("name",),
    fuzzy=FuzzyMode.FUZZY,
    threshold=75,
))

Pipeline (Filter + Sort + Search + Paginate)

Compose all operations in a single call:

from pypaginate import OffsetParams, FilterSpec, SortSpec, SortDirection
from pypaginate.engine.pipeline import SyncPipeline
from pypaginate.engine.paginator import Paginator
from pypaginate.adapters.memory import (
    MemoryBackend,
    MemoryFilterBackend,
    MemorySortBackend,
)

pipeline = SyncPipeline(
    Paginator(MemoryBackend()),
    filter_backend=MemoryFilterBackend(),
    sort_backend=MemorySortBackend(),
)

page = pipeline.execute(
    users,
    OffsetParams(page=1, limit=10),
    filters=[FilterSpec(field="status", value="active")],
    sorting=[SortSpec(field="name", direction=SortDirection.ASC)],
)

For async (e.g., SQLAlchemy), use AsyncPipeline with AsyncPaginator.

Architecture

pypaginate/
├── domain/        # Models, specs, enums, protocols (no deps)
├── engine/        # Paginator, cursor paginator, pipeline
├── filtering/     # In-memory filter engine + operators
├── sorting/       # In-memory sort engine
├── search/        # In-memory search engine
└── adapters/
    ├── memory/        # In-memory backends (filter, sort, search)
    ├── sqlalchemy/    # SA backends (offset, cursor, filter, sort, search)
    └── fastapi/       # Annotated dependencies (OffsetDep, FilterDep, etc.)

CI Pipeline

Tiered pipeline with 40+ concurrent jobs across 4 Python versions and 3 operating systems:

                              ┌─────────┐
                              │  Setup  │
                              └────┬────┘
                 ┌─────────────────┼─────────────────┐
                 ▼                 ▼                  ▼
           ┌──────────┐     ┌──────────┐       ┌──────────┐
           │ Quality  │     │ Security │       │ CodeQL   │
           │ ruff+mypy│     │ bandit   │       │          │
           └────┬─────┘     └──────────┘       └──────────┘
        ┌───────┼────────┐
        ▼                ▼
  ┌──────────┐    ┌──────────────────────────────────────────┐
  │ Arch     │    │ Unit Tests (12 jobs)                     │
  │ 72 tests │    │ Python 3.11-3.14 × Linux/macOS/Windows   │
  └──────────┘    └──────────────────┬───────────────────────┘
            ┌──────────┬─────────────┼──────────┬──────────┐
            ▼          ▼             ▼          ▼          ▼
    ┌────────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ ┌───────┐
    │Integration │ │  E2E   │ │PostgreSQL│ │Property│ │Bench  │
    │ 12 jobs    │ │ 6 flows│ │ real DB  │ │Hypothe.│ │293 pts│
    │ 4Py × 3OS │ │        │ │          │ │        │ │       │
    └────────────┘ └────────┘ └──────────┘ └────────┘ └───────┘
Test Suite Jobs Coverage
Unit 12 (4 Python × 3 OS) All modules, parallel execution
Integration 12 (4 Python × 3 OS) Cross-module with real SQLite
E2E 1 Full FastAPI user journeys
PostgreSQL 1 Real Postgres 16 via service container
Property 1 Hypothesis invariant checking
Architecture 1 File limits, imports, protocols
Benchmarks 1 293 perf benchmarks, PR regression alerts
Total 29+ 872+ tests, 85% coverage gate

Live Benchmark Dashboard -- performance tracked on every commit.

Development

git clone https://github.com/CybLow/paginate.git
cd pypaginate
uv sync

# Run all checks
uv run ruff format . && uv run ruff check --fix . && uv run mypy src/ && uv run pytest

# Individual commands
uv run pytest                  # Tests
uv run pytest --cov            # Coverage
uv run ruff format .           # Format
uv run ruff check --fix .      # Lint
uv run mypy src/               # Type check

Contributing

Contributions are welcome! See CONTRIBUTING.md for guidelines.

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Run tests and quality checks (uv run pytest && uv run ruff check .)
  4. Commit with conventional commits (git commit -m 'feat: add amazing feature')
  5. Open a Pull Request

License

MIT -- see LICENSE for details.

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-0.2.1.tar.gz (85.0 kB view details)

Uploaded Source

Built Distributions

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

pypaginate-0.2.1-cp311-abi3-win_arm64.whl (936.4 kB view details)

Uploaded CPython 3.11+Windows ARM64

pypaginate-0.2.1-cp311-abi3-win_amd64.whl (1.0 MB view details)

Uploaded CPython 3.11+Windows x86-64

pypaginate-0.2.1-cp311-abi3-musllinux_1_2_x86_64.whl (1.3 MB view details)

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

pypaginate-0.2.1-cp311-abi3-musllinux_1_2_aarch64.whl (1.2 MB view details)

Uploaded CPython 3.11+musllinux: musl 1.2+ ARM64

pypaginate-0.2.1-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-0.2.1-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.0 MB view details)

Uploaded CPython 3.11+manylinux: glibc 2.17+ ARM64

pypaginate-0.2.1-cp311-abi3-macosx_10_12_x86_64.whl (1.0 MB view details)

Uploaded CPython 3.11+macOS 10.12+ x86-64

File details

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

File metadata

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

File hashes

Hashes for pypaginate-0.2.1.tar.gz
Algorithm Hash digest
SHA256 736e79ef97e903a111e5c35851ccab8391253a6cd2dcb842958f05984c4d17b8
MD5 7f59100a61c752d36892c24e3abecb6e
BLAKE2b-256 d6b0e5c0f83cacb9ee212913e8cac6e79595eec556197ae6893ec1e7d8a59faa

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-0.2.1.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-0.2.1-cp311-abi3-win_arm64.whl.

File metadata

  • Download URL: pypaginate-0.2.1-cp311-abi3-win_arm64.whl
  • Upload date:
  • Size: 936.4 kB
  • 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-0.2.1-cp311-abi3-win_arm64.whl
Algorithm Hash digest
SHA256 c843e31ab5c374b60912adb0f45d848b5eb4fa7d88b43a75569f743de99d502a
MD5 4f858c713675eef7caa2507841aaa698
BLAKE2b-256 7af9b5e10af4d03307567193f3c96dbdcf2cc9c3620b47753ca47580492411a3

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-0.2.1-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-0.2.1-cp311-abi3-win_amd64.whl.

File metadata

  • Download URL: pypaginate-0.2.1-cp311-abi3-win_amd64.whl
  • Upload date:
  • Size: 1.0 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-0.2.1-cp311-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 dcd7c170903273584b798298cf731eef5f017a211624581e12c60bdafa137b32
MD5 7ba35718ea34be31241faee937f54745
BLAKE2b-256 df80428ac0f72cdb84838e9917efb961eb0730d8cce381fd292ac770806d7219

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-0.2.1-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-0.2.1-cp311-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for pypaginate-0.2.1-cp311-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 e2b7a0219a6af9b16a470924e57c3c3f16ddcefadb7e6833c44f434469a63b59
MD5 dca6fbfe360695c2dcb1a15768a900a1
BLAKE2b-256 8aaf77487d730ef2995349343b42639ee233ae59530d4c8c6b53b1b100c47cc0

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-0.2.1-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-0.2.1-cp311-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for pypaginate-0.2.1-cp311-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 e2365f0fa89f1b7a1a2db38e1f6d72e711d1dc43b079e43254fafbfc839e89d9
MD5 d6e24750bd308fc1b5e28f2b4ecc24af
BLAKE2b-256 baa2d300decf2e8d2e1bc7f2325ee8ec9b035d22781cb149e2689059f91173d3

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-0.2.1-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-0.2.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for pypaginate-0.2.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 6301f5542d346f8d8915c8de675bb4a9bb58a5a78abc27fe4413faa3c6e7d51d
MD5 33cc8d6e49846569eaeb53a78e12c519
BLAKE2b-256 c1122b0f76c60e75528907040a25452d28cb7eaaa0eaa4ab2540327b342e0864

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-0.2.1-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-0.2.1-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for pypaginate-0.2.1-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 bc28f281a959755c3ebd896cef8e80aeae9de4f8feb89e066f9b5d58814b60ed
MD5 985b0a2bf42eeb78a25b76c6d58624e0
BLAKE2b-256 60a743b090db2a699b116fe9a615f4cb58f4a589cf8f83cf5d49208fd07b42dc

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-0.2.1-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-0.2.1-cp311-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for pypaginate-0.2.1-cp311-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 d4ca37866c51a98fa6120452b553b354fe0d4c94332424cf89b23605a3a4da29
MD5 0bb3b9c0a170f8927d642c1c08bb1b66
BLAKE2b-256 c8c20cd0871905524357fe2bcddbcc328f96b9317ce58a270ecc6c051ef1b1e1

See more details on using hashes here.

Provenance

The following attestation bundles were made for pypaginate-0.2.1-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