Skip to content

Hello World

The minimal BlackBull app — full ASGI 3.0 form.

myapp.py
from blackbull import BlackBull, Response

app = BlackBull()

@app.route(path='/')
async def hello(conn, receive, send):
    await send(Response(b'Hello, world!'))

if __name__ == '__main__':
    app.run(port=8000)

Run it:

python myapp.py

Hit it:

$ curl localhost:8000/
Hello, world!

That's a complete server: an HTTP/1.1 listener bound on 127.0.0.1:8000 with one route registered. No external server process and no separate framework package — BlackBull is both.

The full triplet

Every HTTP handler receives three arguments:

Argument Type Role
conn Connection The parsed request — method, path, headers, query string, …
receive async callable Reads request body events from the client
send async callable Writes the response back to the client

send accepts a Response object (as above), a NativeResponse, or raw ASGI event dicts; all three work and can be mixed in the same handler.

Connection, not a scope dict

BlackBull threads a typed Connection end to end. It is not subscriptable — conn['headers'] raises TypeError. An ASGI scope dict appears only where BlackBull meets something that speaks ASGI: an external host such as uvicorn, BB_FORCE_ASGI_SCOPE=1, or a middleware that asks for one by naming its first parameter scope (see Middleware).

What's on the Connection

Attribute Type Notes
conn.type str 'http' or 'websocket'
conn.method str 'GET', 'POST', …
conn.path str URL path, e.g. '/tasks/42'
conn.headers Headers Case-insensitive multi-valued header store
conn.query_string bytes Raw query string, parse with urllib.parse.parse_qs
conn.path_params dict Values captured from {name} segments
conn.state dict Per-request scratch space, shared by every layer
await conn.body() bytes The complete request body
conn.stream() async iterator The body one chunk at a time

Middleware passes values to inner layers through conn.state — typical additions are conn.state['user'] (auth result) and conn.state['json'] (parsed body). Setting an attribute or a top-level key instead does not reach the handler.

What Response does

Response(b'Hello, world!') constructs a response object with a sensible default Content-Type (text/html; charset=utf-8) and sets Content-Length from the body. Pass content_type= to override:

return Response(b'{"ok": true}', content_type='application/json')

For JSON specifically, JSONResponse does the json.dumps for you:

from blackbull import JSONResponse

@app.route(path='/health')
async def health(conn, receive, send):
    await send(JSONResponse({'status': 'ok'}))

When you don't need the triplet

Most handlers only use conn, or nothing at all. BlackBull detects this at registration time and lets you drop the boilerplate:

@app.route(path='/')
async def hello():
    return "Hello, world!"

That's the simplified form — see Your First App for the full pattern, including path params, body parameters, and return-value type mapping.

Next