Skip to main content

fastberry

Performance helpers for read-heavy Python web APIs, on both GraphQL and REST.

On large or deeply-nested payloads, the per-instance / per-field overhead of GraphQL and REST frameworks dominates response time — pure CPU cost with no extra database work. fastberry lets you opt specific hot paths out of that machinery.

Two independent helpers, usable separately:

  • fast_path (GraphQL) — skip strawberry-django's django_resolver overhead on hot types. Targets sync Django.
  • fastberry.rest (REST) — read-only nested serialization that assembles the tree from column-projected queries and encodes with orjson. Works on both Django/DRF and FastAPI/SQLAlchemy — the backend is picked from the model class.

Install

The core package has no hard dependencies; pick the extra for the helper you want, and only that stack gets pulled in:

pip install 'fastberry[graphql]'     # GraphQL: fast_path on strawberry-django (+ Django, strawberry)
pip install 'fastberry[rest]'        # REST on Django/DRF (+ Django, orjson)
pip install 'fastberry[sqlalchemy]'  # REST on FastAPI/SQLAlchemy — no Django (+ SQLAlchemy, orjson)

Requires Python 3.10-3.14. Django (4.2+) is only installed by the graphql and rest extras; the sqlalchemy extra is Django-free.


GraphQL: fast_path

strawberry-django wraps every field resolver in django_resolver, which calls in_async_context() on each resolution. Under sync Django that call raises and catches a RuntimeError internally — a fixed per-field CPU cost (roughly a microsecond or two per field on current strawberry-django; more on older versions) with no extra database work. Because it is paid per field, on large or wide result sets it adds up and dominates response time.

Mark hot types with @fast_path (outermost, after @strawberry_django.type) and register FastPathExtension on the schema:

import strawberry
import strawberry_django
from fastberry import fast_path, FastPathExtension

from myapp.models import Stock


@fast_path
@strawberry_django.type(Stock, disable_optimization=True)
class StockType:
    id: int
    title: str


@strawberry.type
class Query:
    stocks: list[StockType] = strawberry_django.field()


schema = strawberry.Schema(query=Query, extensions=[FastPathExtension])

Only types decorated with @fast_path take the fast path; everything else resolves normally. To turn it off, remove the extension (global) or the decorator (per type). Needs the graphql extra (pip install 'fastberry[graphql]').

One decorator instead of two

To avoid stacking @fast_path + @strawberry_django.type on every hot type, use the combined wrapper. It forwards all arguments to strawberry_django and applies fast_path on top:

from fastberry import strawberry_django as fast_strawberry_django

@fast_strawberry_django.type(Stock, disable_optimization=True)
class StockType:
    id: int
    title: str

fast_strawberry_django.type and .interface are exposed. You still register FastPathExtension on the schema once. Needs the graphql extra (pip install 'fastberry[graphql]').

Generate the GraphQL type from the model

If you don't want to hand-write the type at all, decorate the model with fast_schema. It builds a strawberry_django type from the model's fields, applies fast_path, and stores it on the model as __fast_type__:

from fastberry.strawberry_django import fast_schema

@fast_schema
class Stock(models.Model): ...                       # all concrete fields

@fast_schema(fields=["id", "title"], name="StockType")
class Other(models.Model): ...                        # subset + custom name

StockType = Stock.__fast_type__                       # wire into your schema

Extra keyword args (e.g. disable_optimization=True) are forwarded to strawberry_django.type. The decorator returns the model unchanged, so it composes with other model decorators. Needs the graphql extra.

How it works: the field→resolver map is built once, at class-definition time, keyed by the GraphQL type name (info.parent_type.name). Plain fields resolve via a direct getattr; related managers are materialized with .all(); custom resolvers are called directly, bypassing the django_resolver wrapper.

Benchmark (the strawberry_django example; 3 200 stocks, sync Django + Postgres, query optimizer enabled on both sides so only the per-field resolver overhead differs). The win scales with the number of fields resolved per type — wide, hot types benefit most:

Fields / type Without fast-path With fast-path Speedup
1 46.7 ms 37.1 ms 1.26×
8 123.9 ms 78.7 ms 1.57×
32 380.5 ms 214.4 ms 1.77×
64 726.0 ms 403.9 ms 1.80×

The speedup is roughly flat across dataset size (~1.5× on a 5-field type) and grows with query depth and field count; on a wide real schema (~27 fields/type) it reaches ~2.4×. See example/BENCHMARKS.md for the full size/depth/field sweeps.


REST: fastberry.rest

A nested serializer (DRF's ModelSerializer, or hand-rolled object-graph walking) builds an instance at every node of a relational tree and runs a per-field pipeline across the whole tree. On a deep tree (3-4 relations) this becomes the bottleneck even at a few hundred top-level objects.

fastberry.rest declares the output shape once, then at serialization time fetches each level with a single column-projected query, assembles the tree in Python via indexed dicts (no N+1), and encodes with orjson. It is read-only by design — keep validation and writes on DRF or Pydantic.

It works on Django/DRF and FastAPI/SQLAlchemy as equal, first-class backends: you write the FastRest declaration once and the backend is picked from the model class. Needs the rest extra for Django (pip install 'fastberry[rest]') or the sqlalchemy extra for SQLAlchemy (pip install 'fastberry[sqlalchemy]').

Declare the shape (both backends)

from fastberry.rest import FastRest


class ProductRest(FastRest):
    class Meta:
        model = Product
        fields = ["id", "name", "ean"]


class StockRest(FastRest):
    product = ProductRest()                # forward FK

    class Meta:
        model = Stock
        fields = ["id", "title", "amount", "price"]


class SpaceRest(FastRest):
    stocks = StockRest(many=True)          # reverse FK

    class Meta:
        model = Space
        fields = ["id", "name"]


class HouseRest(FastRest):
    spaces = SpaceRest(many=True)

    class Meta:
        model = House
        fields = ["id", "name", "address"]

The same four classes run unchanged whether Product/Stock/Space/House are Django models or SQLAlchemy mapped classes — only the call that drives them differs, as shown below.

Labels, expressions, and M2M (Django)

Beyond nested sub-objects, three field markers emit common shapes without materializing instances — the label/expression is projected by the database:

from decimal import Decimal

from django.db.models import F, Value
from django.db.models.functions import Concat

from fastberry.rest import Expr, FastRest, LabelRef, M2MLabels


class StockRest(FastRest):
    category = LabelRef(label="name")                 # FK  -> {"id", "label"} | None
    tags = M2MLabels(label="name", cap=20)            # M2M -> capped [{"id","label"}, …]
    full = Expr(Concat("title", Value(" "), "size"))  # DB-computed column
    total = Expr(F("amount") * F("price"), decimal=True)

    class Meta:
        model = Stock
        fields = ["id", "title"]
  • LabelRef(relation=None, label="name") — a forward FK as {"id", "label"} (or None), the label projected from related.<label> via a join.
  • Expr(expression, decimal=False) — a value computed by a Django expression (.annotate()); set decimal=True to stringify a Decimal.
  • M2MLabels(relation=None, label="name", cap=20) — a many-to-many as a capped [{"id","label"}, …] list, fetched set-based over the through table (a {"id": None, "label": "+N more"} marker caps it; cap=None = no cap).

Django / DRF

Serialize a queryset directly:

rows = HouseRest.serialize(House.objects.all())       # list[dict]
body = HouseRest.serialize_json(House.objects.all())  # bytes (orjson)

Or skip wiring a schema per view: decorate the model with @fast_rest and use FastJSONRenderer. The view returns a raw queryset/instance; the renderer finds the registered schema and fast-serializes it. Unregistered models fall back to DRF's normal JSON.

from fastberry.rest import fast_rest
from fastberry.rest_renderers import FastJSONRenderer


@fast_rest(fields=["id", "title", "amount"])     # explicit field list
class Stock(models.Model): ...


@fast_rest(depth=2)                               # auto-derive: expand 2 levels
class House(models.Model): ...                    # FKs -> nested objects,
                                                  # reverse FKs -> nested lists


class HouseList(APIView):
    renderer_classes = [FastJSONRenderer]
    def get(self, request):
        return Response(House.objects.all())      # no serializer needed

@fast_rest styles (mutually exclusive):

  • fields=[...] (+ optional nested={"attr": SubSchema}) — you control exactly what is emitted. Recommended when the model has sensitive columns.
  • depth=N — auto-derive from the model's fields/relations, expanding N relation levels (cycles broken by falling back to the FK id). depth=0 emits scalars + FK ids only. Auto-derive emits every field at each expanded level — prefer explicit fields on models with secrets.
  • Omitted — all concrete fields, FKs as ids, no nesting.

You can also register a hand-written nested FastRest for renderer pickup with register_schema(Model, MyRest). Set FastJSONRenderer globally via DRF's DEFAULT_RENDERER_CLASSES to apply it everywhere; only @fast_rest models take the fast path, so it's safe alongside ordinary endpoints.

FastAPI / SQLAlchemy

Declare the same classes against your SQLAlchemy models. SQLAlchemy needs a session, so pass session= to serialize*() (a select() is optional for filtering/ordering; omit the source for all rows):

rows = HouseRest.serialize(session=session)                       # all rows
body = HouseRest.serialize_json(select(House).where(...), session=session)

In FastAPI, hand the bytes straight back — no DRF, no renderer:

@app.get("/houses")
def houses(session: Session = Depends(get_session)):
    return Response(HouseRest.serialize_json(session=session),
                    media_type="application/json")

See the runnable example/fastapi project. (This works only with an ORM fastberry.rest understands — Django or SQLAlchemy — not ORM-less stacks or other ORMs.)

Benchmark

Measured on the django example; the SQLAlchemy backend runs the same column-projected, one-query-per-level fetch. 200 houses × 4 spaces × 8 stocks = 6400 leaves, 4 levels deep, plus an FK to product; full tree queryset → JSON bytes, Postgres:

Approach min ms queries vs DRF+prefetch
DRF nested, no prefetch 1891 7401 0.05×
DRF nested, with prefetch 91.6 4 1.0×
fastberry.rest FastRest 15.9 4 5.8×

The advantage grows with payload size — ~3.4× at 300 leaves up to ~6.9× at 32 000 — while the query count stays flat at 4. See example/BENCHMARKS.md for the full sweep.

Supported relations in this version: forward foreign key (single) and reverse foreign key / one-to-many (many=True), on both the Django and SQLAlchemy backends. ManyToMany is supported as capped {"id","label"} columns via M2MLabels (Django); nested M2M sub-objects (many=True) are not.

Pluggable list provider

fastberry.list_provider.ListProvider is a thin, framework-agnostic adapter that compiles a duck-typed field plan (model, scalar fields, FK labels, M2M labels) into a FastRest and serializes pages from it. It is the integration point for admin/CRUD layers that want fast list rows without importing fastberry types — for example, Theia NG plugs it in via a single LIST_PROVIDER setting.


Caveats

  • @fast_path targets sync Django specifically — async resolution doesn't pay the django_resolver overhead it removes. fastberry.rest has no such restriction; it works on Django/DRF and FastAPI/SQLAlchemy alike.
  • Both helpers are read-optimized. @fast_path skips custom resolver logic; fastberry.rest does no validation. Use them on read-heavy paths; keep complex/write logic on the default framework path.

License

MIT

Metadata

Release files for fastberry 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fastberry 0.5.0
File Size Uploaded Provenance Actions
fastberry-0.5.0.tar.gz 32.3 kB With provenance Details for fastberry-0.5.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for fastberry 0.5.0
File Interpreter ABI Platform Actions
fastberry-0.5.0-py3-none-any.whl Python 3 none any Details for fastberry-0.5.0-py3-none-any.whl

Total release size: 59.7 kB

Release files / fastberry-0.5.0.tar.gz

Download URL fastberry-0.5.0.tar.gz
Size 32.3 kB
Tags Source
SHA-256 checksum
How to use checksums
a99a85b97f6abc7120a2a3da72790e671b2aa42f69c1ba99dde942c374fbde5d
BLAKE2b-256 checksum
How to use checksums
962c04aa33049388afd08eb5b86084d6ea06717594c60fcf14f0628eab80b455
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

Release files / fastberry-0.5.0-py3-none-any.whl

Download URL fastberry-0.5.0-py3-none-any.whl
Size 27.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
af2956571760c73c3b9cfbe4c71f0f2bd80f402ed51fbd41e8fd4e58b3e004fc
BLAKE2b-256 checksum
How to use checksums
e39d49df5e90915ec07d3ccb44aa2615d6e86d9a05c9d206f7b522ab03f68226
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

Security for fastberry 0.5.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for fastberry-0.5.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page