An opinionated, msgspec-first ASGI micro-framework
Project description
What is jero?
jero is a fast and modern Python web framework for building typed JSON/REST APIs on ASGI. You write resources and endpoints as plain classes and annotate handler inputs and outputs with msgspec Structs; jero handles the rest — routing, request/response validation, serialization, auth, and resource lifecycle — and runs under any ASGI server (granian, uvicorn, …).
It's opinionated on purpose, and makes one bet: that being aggressively prescriptive — rather than flexible — is exactly what lets a framework be both extremely fast and a joy to build on. Three pillars, all non-negotiable:
- Speed. Introspection happens once, at startup. The request path is dict lookup → msgspec decode → call → encode, and nothing else is ever added to it.
- Opinionated DX. One blessed way to do each thing, encoded so you can't get it
wrong. Contracts fail loud at startup with a precise
WiringError, never quietly at runtime. - Strict typing. Fully static under pyright-strict — the types are the contract, and the source of the coming OpenAPI spec. If you don't like typing, this isn't your framework.
And no DI container: dependencies are hand-wired in _wire; the framework adds only
lifecycle — the one thing plain Python doesn't give you.
Example
from msgspec import Struct
from jero import BaseApp, Resource
class WidgetPath(Struct):
widget_id: str
class Widget(Struct):
id: str
name: str
class WidgetResource(Resource):
# called as: GET /widgets/{widget_id}
async def read_one(self, path: WidgetPath) -> Widget:
return Widget(id=path.widget_id, name="widget-name")
class App(BaseApp):
async def _wire(self) -> None:
self._include_resource(WidgetResource(), path="/widgets")
app = App()
Run it under any ASGI server, e.g. granian:
granian --interface asgi myapp:app
With a service layer and a factory
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 httpx
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 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: httpx.AsyncClient
async def fetch(self, widget_id: str) -> Widget:
resp = await self._client.get(f"/widgets/{widget_id}")
if resp.status_code == 404:
raise HTTPError(404, "widget not found")
return json_decode(resp.content, type=Widget)
async def create(self, data: WidgetIn) -> Widget:
resp = await self._client.post("/widgets", content=json_encode(data))
return json_decode(resp.content, type=Widget)
@dataclass
class WidgetResource(Resource):
_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(httpx.AsyncClient(base_url="https://api.example.com"))
return WidgetService(client)
class App(BaseApp[Factory]):
async def _wire(self) -> None:
widgets = await self._factory.create_widget_service()
self._include_resource(WidgetResource(widgets), path="/widgets")
app = App()
Performance
jero is fast — very fast. In fact, it co-leads the quickest Python ASGI frameworks and lands within a few percent of a hand-written Go (Gin) service on the same box.
The numbers below are from the authed write path — POST /movies (bearer auth →
msgspec decode → handler → encode → 201) — run natively under granian with a single
worker (Go pinned to GOMAXPROCS=1), driven by oha
at concurrency 200:
| Framework | Requests/sec | Relative to jero |
|---|---|---|
| Go / Gin (reference) | ≈ 45,200 | 1.03× |
| jero | ≈ 44,000 | 1.00× |
| Blacksheep | ≈ 43,000 | 0.98× |
| Litestar | ≈ 22,000 | 0.50× |
| Robyn | ≈ 15,000 | 0.34× |
| FastAPI | ≈ 7,300 | 0.17× |
A statistical tie with Blacksheep, ~2× Litestar, ~3× Robyn, and ~6× idiomatic
FastAPI — at ~97% of raw Go on the same machine (and ~91% on a plain GET). These
are localhost runs and partly client-bound, so treat them as indicative; the
benchmark harness lives in a separate repo.
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jero-0.0.11.tar.gz.
File metadata
- Download URL: jero-0.0.11.tar.gz
- Upload date:
- Size: 28.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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5cd72dd68618ec0fc7e8336d80080e0894455814474c0eb8b6e7063624bed929
|
|
| MD5 |
6190f3a177ab2b65b91ed8ae58ec1f11
|
|
| BLAKE2b-256 |
d5d2b9f6651930ad0313d896b2b861a7e6d369515e3b9f7d4705d0c5ddccefb7
|
File details
Details for the file jero-0.0.11-py3-none-any.whl.
File metadata
- Download URL: jero-0.0.11-py3-none-any.whl
- Upload date:
- Size: 29.7 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
42cb4098c7fa1b21087e3d62bbdc6e7ac7d06fc7315c6fa27d91c6aab4e626ca
|
|
| MD5 |
52eb4ffa5c448559fa70b5b6a28079a1
|
|
| BLAKE2b-256 |
198f29b827c5c08c2caa65821c6fc26c88fe0776a764a73096cd540cf8e60a86
|