Skip to main content
jero

PyPI Build status codecov CodSpeed 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 is an AI-powered (a note on AI usage) framework that 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 peregrine, 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 the 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 msgspec — json, params, path, headers, form, user; malformed → 400, schema-invalid → 422, all resolved once at startup.
  • Typed responses and typed headers — JSONResponse[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; opt any Struct into camelCase on the wire with rename="camel".
  • A real test story — a sync, in-process TestClient (no network), streaming and typed WebSocket 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

from msgspec import Struct
from msgspec.json import decode as json_decode
from msgspec.json import encode as json_encode
from pyreqwest.client import Client, ClientBuilder

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: Client

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

    async def create(self, data: WidgetIn) -> Widget:
        request = self._client.post("/widgets").header("Content-Type", "application/json")
        resp = await request.body_bytes(json_encode(data)).build().send()
        return json_decode(await resp.bytes(), 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(
            ClientBuilder().base_url("https://api.example.com").build()
        )
        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 / Benchmarks

In a side-by-side benchmark against nine other frameworks — Python (Django Bolt, Blacksheep, Litestar, Robyn, FastAPI, Flask), Go (Gin), Bun (Elysia), and Java (Spring Boot) — jero is the fastest Python framework on three of the four scenarios, and ties Django Bolt (within 0.1%) on the fourth. It is the fastest Python ASGI framework in every scenario, out-runs Go's Gin on three of the four, and is the fastest framework of any language on the database scenario. Each panel below is scaled to its own fastest framework; the labels keep the absolute throughput:

Benchmark results: jero is the fastest Python ASGI framework across all four workloads

On the pure framework hot path (a typed JSON GET):

Framework GET /info req/s Relative to jero
elysia (Bun) 161.3k 1.61×
jero 100.4k 1.00×
django-bolt 95.7k 0.95×
gin (Go) 87.6k 0.87×
blacksheep 73.6k 0.73×
litestar 48.9k 0.49×
robyn 43.4k 0.43×
spring-boot (Java) 41.3k 0.41×
fastapi 30.6k 0.30×
flask 27.3k 0.27×

jero leads Django Bolt's built-in Rust server by 5% here and ties it on the authed write; on the proxy and database paths, where the request's time is spent beyond the framework, jero leads it by ~1.3×. Against the frameworks on the same ASGI server (Blacksheep, Litestar, FastAPI) jero leads every scenario, by 1.4× to 4.5×. Only Bun's Elysia is faster on this path, and on the upstream-proxy scenario Go's Gin edges jero by 6% while jero returns a typed, validated payload. That jero beats a single-core Gin on three of four scenarios is a statement about these conditions, not a claim that Python is faster than Go. (The Python rows are from run 20261023T113907; the Gin, Elysia, and Spring Boot rows are from that day's full-fleet run, 20261023T091453.)

These are favourable, constrained conditions — single worker, one dedicated core, best-of-N — and a microbenchmark is not your application. See all four scenarios in the Performance / Benchmarks docs, and the full harness — every framework's source and the complete methodology — at api-benchmarks.

Development

task install   # create the venv and install pre-commit hooks
task check     # lock check + ruff, pyrefly, 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.

Metadata

Release files for jero 0.1.6

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

Source distribution (sdist)

Source distribution for jero 0.1.6
File Size Uploaded Provenance
jero-0.1.6.tar.gz 136.1 kB Without provenance Details

Built distribution (wheel)

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

Total release size: 274.8 kB

Release files / jero-0.1.6.tar.gz

Download URL jero-0.1.6.tar.gz
Size 136.1 kB
Tags Source
SHA-256 checksum
How to use checksums
14495e2471169e41e881e15bec255c8cc48e46215771b97bda7dc9c1f7f9b3d0
BLAKE2b-256 checksum
How to use checksums
b96fc339ebb64fb65bf48dfc0bfbebe788f49425cee1eabc6b06ce4cb2485a46
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","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}

Release files / jero-0.1.6-py3-none-any.whl

Download URL jero-0.1.6-py3-none-any.whl
Size 138.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d7a4bbf0584391cf0014d71dc5d84f4734f7d17c5a324f28582e70dfd334302f
BLAKE2b-256 checksum
How to use checksums
fe0580bb9d107d265ee50e7d20698db69fef41666a98bc5d0ecade58ce38313f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","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}

Security for jero 0.1.6

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

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.

Release history Release notifications | RSS feed

This release

0.1.6 This release

2 release files No provenance

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