# BlackSheep: an ASGI Python framework that compiles its own hot paths

> An asyncio web framework built by Roberto Prevato, with Cython extensions for URL parsing and headers, a dependency injection container, and a Wasmtime-shaped API. Here is how the pieces fit.

**Neoteroi/BlackSheep** — Fast ASGI web framework for Python

- Repository: https://github.com/Neoteroi/BlackSheep
- Website: https://www.neoteroi.dev/blacksheep/
- Stars: 2,360 · Forks: 100
- Language: Python
- License: MIT
- Published: 2026-10-07 · Updated: 2026-10-07 · Language: en
- Canonical page: https://hysenlabs.com/projects/neoteroi-blacksheep

## A framework built around type annotations for request binding

The README describes BlackSheep as an asynchronous framework for event-based Python applications, inspired by Flask, ASP.NET Core and uvloop's networking work. The smallest example is a decorator and a return value, with no response object to construct:

```python
from datetime import datetime, timezone

from blacksheep import Application, get


app = Application()

@get("/")
async def home():
    return f"Hello, World! {datetime.now(timezone.utc).isoformat()}"
```

Returning a string rather than building a response is a small ergonomic decision with a large effect on how a handler reads. The interesting part is what comes next: parameters are bound by type annotation or by naming convention, so a handler asking for a dataclass with a JSON body gets an instance of it.

```python
from dataclasses import dataclass

from blacksheep import Application, FromJSON, FromQuery, get, post


app = Application()


@dataclass
class CreateCatInput:
    name: str


@post("/api/cats")
async def example(data: FromJSON[CreateCatInput]):
    ...
```

Route parameters bind by name match, and query parameters bind implicitly when nothing else claims them, with `page`, `size` and `search` coming from the query string because no route segment matches those names. `FromQuery` makes that explicit when you want it. The framework also offers a dependency injection container, and `pyproject.toml` shows it is a separate project, `rodi`, pinned at `~=2.0.8`.

This is a real design position rather than a feature list. Type annotations carry routing information, which means a handler signature documents its own inputs.

## Cython extensions for the five hottest modules

The most interesting engineering decision in the repository is not in the README at all. It is in setup.py, which compiles Cython extensions for `blacksheep.url`, `blacksheep.exceptions`, `blacksheep.headers`, `blacksheep.cookies`, `blacksheep.contents` and `blacksheep.messages`, each at optimisation level two.

That list is a performance argument made concrete. URL parsing, header handling and message construction are the code paths every request touches, so they are exactly what you would accelerate if you were willing to accept a C toolchain. Six modules, none of them the routing or application logic, which suggests the author measured rather than guessed.

The build is conditional in two ways, and both conditions are in setup.py rather than documented prominently. The extensions compile only when the runtime is CPython, and only when the environment variable `BLACKSHEEP_NO_EXTENSIONS` is not set to 1. The stated reason in the file is PyPy support, and the README corroborates it: before version 2.3.1 BlackSheep only ran on CPython and always depended on httptools, and from 2.3.1 onwards it supports PyPy with httptools made optional.

The Makefile drives the same pipeline from the other direction:

```bash
cyt:
	cython blacksheep/url.pyx
	cython blacksheep/exceptions.pyx
	cython blacksheep/headers.pyx

compile: cyt
	python3 setup.py build_ext --inplace
```

There is a `pack` target that sets `BLACKSHEEP_NO_EXTENSIONS=1` before building an sdist, which confirms the extensions are an install-time optimisation rather than a requirement. If you install from source and the build fails, that variable is the documented way to fall back to pure Python.

## Dependencies that changed shape twice in two versions

The README has a Dependencies section that reads like a changelog, and it is the section most worth reading carefully.

Before 2.3.1, the framework supported only CPython and always required httptools. From 2.3.1, PyPy is supported and httptools became optional, with a recommendation to install it on CPython for faster URL parsing. From 2.5.0, the HTTP client gained HTTP/2 support and requires both `h11` and `h2`. The README then says the best performance comes from running on PyPy with either Socketify or Granian as the server, linking to an issue for detail.

`pyproject.toml` reflects the current state. It requires Python 3.10 or newer and lists classifiers from 3.10 through 3.14, for both CPython and PyPy. httptools carries a marker so it is skipped on PyPy:

```toml
    "httptools>=0.7.1; platform_python_implementation != 'PyPy'",
```

One discrepancy is worth flagging rather than resolving. The packaging metadata pins `rodi~=2.0.8`, while requirements.txt, which appears to be a development and test manifest, lists `rodi~=2.0.2`. Those are different minor lines of the same dependency. If you are chasing a dependency resolution failure rather than reading for context, the version that ships to users is the one in pyproject.toml.

requirements.txt is otherwise a full test environment rather than a runtime list: it pins Flask with the async extra, Hypercorn, Starlette, pytest and its asyncio plugin, Cython, and a set of exact pins such as `h11==0.16.0`, `PyJWT==2.13.0` and `websockets~=15.0.1`. Useful context for how the project tests itself, and not something to install into an application.

## OpenAPI generation and auth strategies built into the app object

Documentation generation is not a plugin here; it is a feature of the application object, backed by a separate package named `essentials-openapi`. That one is pinned in the dependency list alongside `essentials` and `guardpost`, the latter being the library behind the authentication strategies.

Auth is configured by chaining off the app:

```python
app.use_authentication()\
    .add(ExampleAuthenticationHandler())


app.use_authorization()\
    .add(AdminsPolicy())


@auth("admin")
@get("/")
async def only_for_admins():
    ...
```

The decorator order matters and reads bottom up: the route decorator sits closest to the function, and the `auth` decorator above it declares the scheme or policy name. The README documents built-in support for OpenID Connect authentication and JWT Bearer tokens, with a separate authorization page covering policies.

The structure here is the notable part. Authentication and authorization are two separate registration surfaces, which means the scheme that identifies a user and the policy that decides what they may do are configured independently. For an application with more than one audience, or with an identity provider that changes, that separation is worth more than the number of built-in strategies.

The ASGI story is settled early in the README: BlackSheep belongs to the category of ASGI web frameworks, so it requires an ASGI HTTP server such as uvicorn, hypercorn or granian. The application is then started with that server's own conventions, such as `uvicorn server:app`. For production, the README defers to the ASGI server's documentation rather than prescribing one.

## A CLI, two project templates and a maintained release line

Project scaffolding is a separate distribution. `pip install blacksheep-cli` gives you a `blacksheep create` command that bootstraps from templates, and the CLI supports custom templates using the same sources Cookiecutter supports. That reuse is worth noting: it means a project that already has a Cookiecutter template can be used as a BlackSheep starting point without translating it.

Two official templates are linked: an MVC template and an empty project template, in separate repositories. The MVC one is the interesting default for a reader who wants to see how the author would structure something with a model layer, since the framework itself has no opinions about database access and the dependency injection container is where a data layer would attach.

The repository is not archived and the last push was 2026-09-21. Releases are v2.6.3 from 2026-06-04, v2.6.2 from 2026-02-25 and v2.6.1 from 2026-02-23, a cadence that suggests a stable line with frequent small work rather than large rewrites. GitHub reports seven open issues against 2,358 stars.

The Makefile gives a good picture of the test approach, which is unusually split for a Python project. There is a plain `test` target running `pytest tests/`, and separately an `itest` target that sets `APP_DEFAULT_ROUTER=false` before running `pytest itests/`. Integration tests run against a different router configuration, which is a cheap way to catch code that has quietly assumed the default router.

Lint and format targets use isort and black, in that order, across the package and both test directories. The `testrelease` target uploads to testpypi and `release` uploads to pypi, so publishing is a make target rather than a script to write each time.

## What the README decides and what the docs site owns

The README settles the things a reader needs in the first ten minutes: how to install, what a handler looks like, which runtime to prefer, which ASGI server to run it under, and where auth and OpenAPI documentation lives. That is a complete orientation in about a screen and a half.

Everything past that belongs to the documentation site. Request binding has its own page. Dependency injection has its own page. Authentication, authorization and OpenAPI generation each have theirs. The README is careful to link rather than summarise, which is the right call for a framework whose API surface is wide enough that summarising it would mislead.

The place where the repository itself is more informative than the README is the build system. setup.py and the Makefile together answer a question the README does not ask: what happens on your machine at install time. Six Cython modules compiled at optimisation level two, gated on CPython and on an environment variable, with a pure Python fallback path. If you are deciding whether this framework suits a constrained deployment, that is the section to read before the feature list.

The version history helps here too. The PyPy support and the optional httptools both arrived in 2.3.1, and HTTP/2 in the client in 2.5.0. A framework whose dependency story has changed twice recently is one where the README's install instructions deserve a second read, and where the classifier list in pyproject.toml is a more reliable guide to supported versions than a tutorial written a year ago.

## Conclusion

BlackSheep is one of the more coherent answers to the question of what an ASGI Python framework looks like when the author cares about the allocation path. The Cython story is the part that distinguishes it: not just an optional accelerator but a build step with its own Makefile targets and an escape hatch, which tells you the author expected it to cause friction. Read the dependency section of the README before choosing an install, because the httptools and runtime guidance changed twice in recent versions and the difference affects performance claims. For anything beyond a hello world, the docs site rather than the repository is where you will actually spend your time.

## FAQ

### Is BlackSheep a Django or Flask replacement?

It borrows from Flask's minimalism and from ASP.NET Core, but it sits closer to Starlette or FastAPI in shape. Like FastAPI it derives request handling from type annotations, and like Starlette it expects an external ASGI server such as uvicorn, hypercorn or granian. It has no built-in ORM or migration tooling.

### Does BlackSheep need an ASGI server to run?

Yes. The framework is an ASGI application, so it needs a server like uvicorn, hypercorn or granian in front of it. The README recommends PyPy together with Socketify or Granian for the best performance, and defers production deployment guidance to the chosen server's own documentation.

### Why did installing BlackSheep compile Cython code?

setup.py builds extensions for the URL, exception, header, cookie, content and message modules when the runtime is CPython, to speed up the per-request path. Setting the environment variable BLACKSHEEP_NO_EXTENSIONS=1 skips them, which is the fallback the project itself uses when building an sdist.

### Does BlackSheep support WebSockets and HTTP/2?

The dependency list includes the websockets package in the test manifest, and from version 2.5.0 the HTTP client added HTTP/2 support, which pulls in h11 and h2. The README documents the client's HTTP/2 support rather than server-side protocol negotiation.

### How do I create a new BlackSheep project?

Install blacksheep-cli, then run blacksheep create to bootstrap from a template. Official MVC and empty-project templates exist as separate repositories, and the CLI can also read custom templates from the same sources Cookiecutter supports.

## Sources

- [License: MIT](https://github.com/Neoteroi/BlackSheep/blob/main/LICENSE)
- [Neoteroi/BlackSheep on GitHub](https://github.com/Neoteroi/BlackSheep)
- [Project website](https://www.neoteroi.dev/blacksheep/)
- [README](https://github.com/Neoteroi/BlackSheep/blob/main/README.md)
- [Releases](https://github.com/Neoteroi/BlackSheep/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/neoteroi-blacksheep
