Skip to main content

slick-framework

A slick, minimal ASGI web framework for Python, built only on the standard library.

slick covers what most small services need: routing with typed path parameters, request and response objects, JSON in and out, middleware, lifespan hooks, an in-process test client and a development server. It has zero runtime dependencies, and the core is a few hundred lines you can read in one sitting.

from slick import Slick

app = Slick()

@app.get("/hello/{name}")
async def hello(request, name: str):
    return {"message": f"Hello, {name}!"}

Features

  • ASGI 3 app: runs under uvicorn, hypercorn or any other ASGI server.
  • Routing with {name}, {id:int}, {x:float} and {rest:path} parameters, converted values, automatic HEAD for GET routes, and proper 404/405 (with Allow) responses.
  • Path params injected as keyword arguments when the handler's signature names them.
  • Return plain values: dict/list becomes JSON, str becomes text, None becomes 204, and (body, status[, headers]) tuples set the status and headers.
  • Request object with headers, query params, cookies, await request.json() / .form() / .body().
  • Responses: Response, JSONResponse, PlainTextResponse, HTMLResponse, RedirectResponse, plus cookies.
  • Middleware: plain async def mw(request, call_next) functions.
  • Errors: HTTPException, plus custom handlers for any exception type.
  • Lifespan hooks: @app.on_startup / @app.on_shutdown, and app.state / request.state.
  • Sub-routers: Router() and app.include_router(router, prefix="/api").
  • TestClient: synchronous and in-process, with no sockets and nothing extra to install.
  • serve(): a stdlib-only development server, so examples run straight away.
  • Sync handlers are supported too (they run in a worker thread). Fully type-hinted (py.typed).

Install

pip install slick-framework            # core, no dependencies
pip install "slick-framework[server]"  # optional: adds uvicorn for production serving

Requires Python 3.9+.

Quickstart

# app.py
from slick import HTTPException, Request, Slick, serve

app = Slick()
BOOKS = {1: {"id": 1, "title": "Dune"}}

@app.get("/books/{book_id:int}")
async def get_book(request: Request, book_id: int):
    if book_id not in BOOKS:
        raise HTTPException(404, "No such book")
    return BOOKS[book_id]

@app.post("/books")
async def add_book(request: Request):
    data = await request.json()          # malformed JSON -> 400 automatically
    book = {"id": len(BOOKS) + 1, "title": data["title"]}
    BOOKS[book["id"]] = book
    return book, 201

if __name__ == "__main__":
    serve(app, port=8000)                # or: uvicorn app:app

Test it without a network:

from slick import TestClient
from app import app

with TestClient(app) as client:          # `with` also runs startup/shutdown hooks
    assert client.get("/books/1").json() == {"id": 1, "title": "Dune"}
    assert client.post("/books", json={"title": "Emma"}).status_code == 201
    assert client.get("/books/99").status_code == 404

A fuller example (a todo CRUD API with a sub-router, middleware and startup hook) is in examples/todo_app.py:

python3 examples/todo_app.py   # after `pip install -e .`; or prefix with PYTHONPATH=src

API overview

Everything below is importable from slick.

Slick(*, debug=False)

The application, and an ASGI callable. It subclasses Router, so it has all of the router methods.

Member Description
@app.get(path, name=None), .post, .put, .patch, .delete Register a handler for one method. GET also answers HEAD.
@app.route(path, methods=None, name=None) Register for several methods (default ["GET"]).
app.add_route(path, handler, methods=None, name=None) -> Route Non-decorator form.
app.include_router(router, prefix="") Copy a Router's routes in under prefix.
app.url_for(name, **params) -> str Build a path from a route name. The name defaults to the handler's __name__.
@app.middleware Register async def mw(request, call_next) -> Response. The first registered is outermost.
@app.exception_handler(ExcClass) Register handler(request, exc), sync or async, for that class and its subclasses.
@app.on_startup, @app.on_shutdown Lifespan hooks taking no arguments, sync or async.
app.state A State attribute bag for app-wide objects.
app.debug If true, an unhandled error returns its traceback as a 500 text body.

Handlers are handler(request, **path_params), async def or plain def. A path param is passed as a keyword argument only if the signature names it or accepts **kwargs. Otherwise read it from request.path_params. A handler may return:

Return value Response
Response (or subclass) sent as-is
dict / list JSONResponse
str PlainTextResponse
bytes Response with application/octet-stream
None empty 204 No Content
(value, status) / (value, status, headers) the above, with that status and extra headers

Middleware may return the same kinds of value.

Path converters: str (default, one segment), int (optionally negative), float, path (can include /). Matching is exact, with no trailing-slash redirects. Routes are tried in registration order.

Errors: HTTPException and unmatched routes render as {"detail": ...} JSON, and middleware sees them as normal responses. Any other uncaught exception becomes a 500 {"detail": "Internal Server Error"}, is logged on the slick logger and is re-raised to the server.

Router()

A collection of routes that has the same get/post/put/patch/delete/route/add_route/include_router/url_for methods as the app, plus resolve(method, path) -> (Route, params). Route(path, handler, methods=None, name=None) provides .match(path) and .url_for(**params).

Request

Member Description
method, path, url Upper-case method, decoded path, and the reconstructed absolute URL.
headers Case-insensitive Headers.
query_params QueryParams: get(key, default) returns the first value, getlist(key) returns all values.
path_params dict of converted route parameters.
cookies dict parsed from the Cookie header.
client (host, port) or None.
state A per-request State, for example set by middleware.
app, scope The Slick app and the raw ASGI scope.
await body(), await text() The raw body (cached) and the body decoded as UTF-8.
await json() The parsed body. Raises HTTPException(400) on invalid JSON.
await form() A dict from a urlencoded body.

Responses

  • Response(content=None, status_code=200, headers=None, media_type=None): content is bytes, str or None. It has .status_code, .body, a mutable .headers (Headers) and .set_cookie(key, value="", *, max_age=None, path="/", httponly=False, secure=False, samesite="lax").
  • JSONResponse(content, status_code=200, headers=None, *, indent=None, default=None) produces compact UTF-8 JSON by default.
  • PlainTextResponse, HTMLResponse take the same arguments as Response.
  • RedirectResponse(url, status_code=307, headers=None).
  • to_response(value) applies the return-value rules above.

HTTPException(status_code, detail=None, headers=None)

When detail is omitted it defaults to the standard reason phrase (for example "Not Found").

TestClient(app, base_url="http://testserver", raise_server_exceptions=True)

  • .get/.head/.post/.put/.patch/.delete/.options(url, *, params=None, headers=None, json=None, data=None, cookies=None) and .request(method, url, ...) return a TestResponse. data takes a form mapping, str or bytes.
  • with TestClient(app) as client: runs the app's startup and shutdown hooks. .close() releases the event loop.
  • Set-Cookie values are stored in client.cookies and sent on later requests.
  • With raise_server_exceptions=False you get the 500 response instead of the exception.
  • TestResponse has .status_code, .headers, .content, .text, .json() and .ok.

serve(app, host="127.0.0.1", port=8000)

A threaded development server built on http.server. It runs lifespan hooks and stops on Ctrl+C. It buffers whole bodies and is not meant for production; use uvicorn yourmodule:app there.

Data structures

Headers (case-insensitive and multi-valued: getlist, append, multi_items, raw, from_raw), QueryParams (getlist, multi_items), and State (an attribute bag with .get(name, default)).

Development

python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest                          # or, with no installs at all:
PYTHONPATH=src python3 -m unittest discover -s tests

License

MIT

Metadata

Release files for slick-framework 0.1.0

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

Source distribution (sdist)

Source distribution for slick-framework 0.1.0
File Size Uploaded Provenance
slick_framework-0.1.0.tar.gz 28.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for slick-framework 0.1.0
File Interpreter ABI Platform
slick_framework-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 52.2 kB

Release files / slick_framework-0.1.0.tar.gz

Download URL slick_framework-0.1.0.tar.gz
Size 28.1 kB
Tags Source
SHA-256 checksum
How to use checksums
a1528c707621dfe7be385eeb3e89f4ea0d5e024d6c72d626084bda0381dd85c1
BLAKE2b-256 checksum
How to use checksums
58a27df4ec870d9ca1841562df0944e75868fa14be78b15491cce9828c8c882d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / slick_framework-0.1.0-py3-none-any.whl

Download URL slick_framework-0.1.0-py3-none-any.whl
Size 24.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bc2bc50e38be971136770c49eebc54cceabe7df700a6a394f34b98e262603dd7
BLAKE2b-256 checksum
How to use checksums
fac7ee07cb4c47dafcce4e33720d70ce515d5121386c2d24fdb30e75d05f37a5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Security for slick-framework 0.1.0

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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

0.0.1

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