Skip to main content
jero

PyPI Build status codecov Python versions License

An opinionated, msgspec-first ASGI micro-framework for Python 3.13+.


Engineered for performance from the ground up
Strictly typed end to end
A joy to build on
GitHub · Documentation

jero builds typed JSON/REST APIs from plain classes. Annotate your handlers with msgspec Structs — jero does the rest: routing, validation, serialization, auth, streaming, and resource lifecycle.

from msgspec import Struct

from jero import BaseApp, Resource


class Widget(Struct):
    id: str
    name: str


class WidgetPath(Struct):
    widget_id: str


class WidgetResource(Resource, path="/widgets"):
    async def read_one(self, path: WidgetPath) -> Widget:  # GET /widgets/{widget_id}
        return Widget(id=path.widget_id, name="gizmo")


class App(BaseApp):
    async def wire(self) -> None:
        self.include_resource(WidgetResource())


app = App()
granian --interface asgi myapp:app    # or uvicorn, or any ASGI server

No decorators, no dict returns, no runtime surprises — the Struct types are the request/response contract, and they're verified at startup.

Why jero?

⚡ Fast The fastest Python ASGI framework across every workload in our benchmark. All introspection happens once, at startup; the request path is just dict lookup → decode → call → encode.
🎯 Opinionated One blessed way to do each thing, so you can't get it wrong. Contracts fail loud at startup with a precise WiringError, never quietly at runtime.
🔒 Typed Fully static under pyright-strict, leaning hard into modern Python typing — PEP 695 generics (JSONResponse[Body, Headers], BaseApp[Factory]), bounded type-params, generic inheritance, Protocols. A handler's signature is its schema, and the source of the generated OpenAPI spec.

No DI container, either: dependencies are hand-wired in wire; the framework adds only lifecycle — the one thing plain Python doesn't give you.

What you get

  • Resources & Endpoints — REST CRUD by method name, or bare verbs for one-off routes.
  • Bind by name, validated by msgspecjson, params, path, headers, form, user; malformed → 400, schema-invalid → 422, all resolved once at startup.
  • Typed responses and typed headersJSONResponse[Body, Headers] keeps both schemas (no erasure), status_code overrides the status, and raw_headers is the escape hatch for cookies and the exotic tail.
  • Streaming, typed end to end — NDJSON, Server-Sent Events, and raw byte streams, with lifecycle teardown and client-disconnect handling done for you.
  • Multipart forms & uploads — typed parts, file uploads, per-part headers.
  • Auth checked at startup — the user type is verified against your authenticator before a single request is served, not at runtime.
  • OpenAPI 3.1, derived — one include_openapi call serves the spec and a Scalar UI, built from your types, docstrings, and msgspec.Meta constraints — no decorators.
  • Lifecycle without a DI container — hand-wire in wire, open resources on exit stacks, group construction in a BaseFactory.
  • REST semantics for free — 404/400/422/401/405, auto HEAD + OPTIONS, camelCase on the wire.
  • A real test story — a sync, in-process TestClient (no socket), streaming support, and a factory= seam for mocking.

Start with Getting Started, or browse the full Guide.

A real app

For anything real, a resource delegates to a service, and a Factory builds that service — opening any resources it needs (HTTP clients, DB pools, …) on the app's exit stacks, which jero closes in reverse at shutdown. The app is parameterised with the factory type (BaseApp[Factory]), exposing it as self.factory in wire.

from dataclasses import dataclass

import niquests
from msgspec import Struct
from msgspec.json import decode as json_decode
from msgspec.json import encode as json_encode

from jero import BaseApp, BaseFactory, HTTPError, Resource


class WidgetNotFoundError(
    HTTPError,
    type="widget-not-found",
    title="Widget not found",
    status=404,
): ...


class WidgetPath(Struct):
    widget_id: str


class WidgetIn(Struct):
    name: str


class Widget(WidgetIn):
    id: str


@dataclass
class WidgetService:
    """Owns the upstream HTTP client; built once by the factory."""

    _client: niquests.AsyncSession

    async def fetch(self, widget_id: str) -> Widget:
        resp = await self._client.get(f"/widgets/{widget_id}")
        if resp.status_code == 404:
            raise WidgetNotFoundError()
        return json_decode(resp.content, type=Widget)

    async def create(self, data: WidgetIn) -> Widget:
        resp = await self._client.post("/widgets", data=json_encode(data))
        return json_decode(resp.content, type=Widget)


@dataclass
class WidgetResource(Resource, path="/widgets"):
    _service: WidgetService

    # called as: POST /widgets
    async def create(self, json: WidgetIn) -> Widget:
        return await self._service.create(json)

    # called as: GET /widgets/{widget_id}
    async def read_one(self, path: WidgetPath) -> Widget:
        return await self._service.fetch(path.widget_id)


class Factory(BaseFactory):
    async def create_widget_service(self) -> WidgetService:
        client = await self.aenter(niquests.AsyncSession(base_url="https://api.example.com"))
        return WidgetService(client)


class App(BaseApp[Factory]):
    async def wire(self) -> None:
        widget_service = await self.factory.create_widget_service()
        self.include_resource(WidgetResource(widget_service))


app = App()

Performance

In a side-by-side benchmark against six other frameworks — Python (Litestar, FastAPI, Blacksheep, Flask), Go (Gin), and Bun (Elysia) — jero is the fastest Python framework in every scenario tested. On the pure framework hot path (a typed JSON GET):

Framework GET /info req/s Relative to jero
gin (Go) 96.5k 1.42×
elysia (Bun) 88.1k 1.30×
jero 67.7k 1.00×
blacksheep 54.6k 0.81×
litestar 37.1k 0.55×
fastapi 26.9k 0.40×
flask 16.5k 0.24×

Go and Bun top the raw table (both finished with CPU headroom; the Python frameworks ran at their genuine single-core ceilings). On the upstream-proxy scenario — with every Python framework on the same Rust HTTP client — jero relays within ~10% of Go at an equal p99. On the database scenario Go pulls well clear, because there the bottleneck is the database driver, not the framework. jero stays the fastest Python option, but it isn't as fast as Go in general, and we're not claiming it is.

These are favourable, constrained conditions — single worker, one dedicated core, best-of-N — and a microbenchmark is not your application. See the full methodology and all four scenarios in the Performance docs.

Development

task install   # create the venv and install pre-commit hooks
task check     # lock check + ruff, pyright, deptry, pylint (via prek)
task test      # run the test suite with coverage

See AGENTS.md for the design philosophy and the contract, and style-guide.md for project conventions.

Download files

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

Source Distribution

jero-0.0.43.tar.gz (64.5 kB view details)

Uploaded Source

Built Distribution

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

jero-0.0.43-py3-none-any.whl (69.3 kB view details)

Uploaded Python 3

File details

Details for the file jero-0.0.43.tar.gz.

File metadata

  • Download URL: jero-0.0.43.tar.gz
  • Upload date:
  • Size: 64.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for jero-0.0.43.tar.gz
Algorithm Hash digest
SHA256 94b5dde5f896112aac994361795f31d247f50a9b20548b6b0d0ef8ea88124076
MD5 b0ade3d44f0f43a6131c18250327226b
BLAKE2b-256 52f35541742aa265d1c6ce9381694c7bc8798953231c50f71a7d86eca3ec8cd8

See more details on using hashes here.

File details

Details for the file jero-0.0.43-py3-none-any.whl.

File metadata

  • Download URL: jero-0.0.43-py3-none-any.whl
  • Upload date:
  • Size: 69.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for jero-0.0.43-py3-none-any.whl
Algorithm Hash digest
SHA256 9e4d4e6f07c71ce6130a16ef4ab76548f6fcb8129a348fc568499a747f5e15b6
MD5 10eef30538e48bb144c7d6c33f63ce62
BLAKE2b-256 d5cb6e78d9849fcb8d58a71ac4da6b7b3c6e8d03ab4206a3460c46ecf9ae5ba6

See more details on using hashes here.

Supported by

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