Flask#
Layered configuration#
Litestar uses a layered architecture. The parts used to group routes can also be used to hierarchically organize an application. Settings and configuration like dependencies, exception handlers, guards, middleware, response cookies and headers, lifecycle hooks, OpenAPI configuration and many more can be defined on any layer, and will be merged upon registration.
The layers in hierarchical order are
Application (
Litestar)Router/Controller(these are on the same level and can be arbitrarily nested)Handler (
BaseRouteHandler)
When the same configuration is set on multiple layers, the value set closest to the handler takes precedence.
Route handlers#
Routes are declared with method-specific decorators
(get(), post(), …) and registered on
an application, router or controller:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Index Page"
@app.route("/hello")
def hello():
return "Hello, World"
from litestar import Litestar, get
@get("/", sync_to_thread=False)
def index() -> str:
return "Index Page"
@get("/hello", sync_to_thread=False)
def hello() -> str:
return "Hello, World"
app = Litestar(route_handlers=[index, hello])
Routers and
controllers group handlers and bind them with
a shared set of configuration. They are part of Litestar’s layered architecture.
See also
Path parameters#
Flask declares converters inline: <int:post_id>. Litestar calls these
“path parameters”, which are declared like /post/{post_id:int}. The handler can
request a path parameter by annotating a function parameter with
FromPath. By default, Litestar will try to inject a path
parameter matching the function parameter name, but you can also specify an alias
(see Aliasing).
Note
The type defined in the path parameter does not have to match the function parameter. The path parameter is what defines the inout type, and what’s reflected in the OpenAPI schema. The function parameter is what will be validated against.
from flask import Flask
app = Flask(__name__)
@app.route("/user/<username>")
def show_user_profile(username):
return f"User {username}"
@app.route("/post/<int:post_id>")
def show_post(post_id):
return f"Post {post_id}"
@app.route("/path/<path:subpath>")
def show_subpath(subpath):
return f"Subpath {subpath}"
import pathlib
from litestar import Litestar, get
from litestar.params import FromPath
@get("/user/{username:str}", sync_to_thread=False)
def show_user_profile(username: FromPath[str]) -> str:
return f"User {username}"
@get("/post/{post_id:int}", sync_to_thread=False)
def show_post(post_id: FromPath[int]) -> str:
return f"Post {post_id}"
@get("/path/{subpath:path}", sync_to_thread=False)
def show_subpath(subpath: FromPath[pathlib.Path]) -> str:
return f"Subpath {subpath}"
app = Litestar(route_handlers=[show_user_profile, show_post, show_subpath])
See also
Sync and async handlers#
Both Flask and Litestar support synchronous and asynchronous functions, but they are handled differently. Flask implements the WSGI protocol, which is synchronous by nature, while Litestar implements ASGI, an asynchronous protocol.
When Flask runs asynchronous endpoints, they run isolated from another. The handling of requests is still synchronous. Litestar is asynchronous throughout, meaning one worker can handle many asynchronous requests concurrently.
Litestar can also handle synchronous endpoints without blocking, by running them in a
thread pool. To enable this, set sync_to_thread=True on the handler decorator.
Important
A sync handler without sync_to_thread set emits a runtime warning, since
Litestar cannot tell whether the function blocks. Pass sync_to_thread=False to
suppress the warning when the body genuinely does not block.
@app.get("/")
def slow_handler():
# blocking I/O on the worker
time.sleep(1)
return {"hello": "world"}
from litestar import get
@get("/", sync_to_thread=True)
def slow_handler() -> dict[str, str]:
# blocking on a thread, event loop remains free
time.sleep(1)
return {"hello": "world"}
Lifespan#
Flask has @before_first_request for startup work and no matching hook for shutdown.
Litestar accepts one or more async context managers through
lifespan; setup goes before the yield, teardown
after.
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from litestar import Litestar, get
@asynccontextmanager
async def lifespan(app: Litestar) -> AsyncIterator[None]:
# Setup: open database pool, warm caches, etc.
yield
# Teardown: close resources.
@get("/")
async def index() -> dict[str, str]:
return {"hello": "world"}
app = Litestar(route_handlers=[index], lifespan=[lifespan])
Accessing the request#
Flask exposes the current request through flask.request, a thread-local proxy. In
Litestar a handler that needs the request asks for it by name; the parameter is typed
as Request.
from flask import Flask, request
app = Flask(__name__)
@app.get("/")
def index():
return {"method": request.method}
from litestar import Litestar, Request, get
@get("/", sync_to_thread=False)
def index(request: Request) -> dict[str, str]:
return {"method": request.method}
app = Litestar(route_handlers=[index])
Request attribute reference#
The table maps each flask.Request attribute to its Litestar counterpart
Flask |
Litestar |
|---|---|
|
Injected via function parameters marked with |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Injected via |
|
Injected via the |
|
Injected via the |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
See also
Per-request state#
Flask uses flask.g for values that should live for the duration of a single request.
Litestar uses request.state, a mutable mapping attached to the request.
from flask import Flask, g, request
app = Flask(__name__)
@app.before_request
def set_user() -> None:
g.user = request.headers.get("x-user", "anonymous")
@app.get("/")
def index():
return {"user": g.user}
from litestar import Litestar, Request, get
def set_user_on_request(request: Request) -> None:
request.state["user"] = request.headers.get("x-user", "anonymous")
@get("/", sync_to_thread=False)
def index(request: Request) -> dict[str, str | None]:
return {"user": request.state["user"]}
app = Litestar(route_handlers=[index], before_request=set_user_on_request)
The current application#
flask.current_app is a thread-local proxy. Litestar exposes the application as
request.app, or as an app parameter on lifecycle hooks and middleware factories.
File uploads#
Flask exposes uploaded files through request.files. Litestar binds them to a
data parameter typed as UploadFile (or a list of
them), with media_type set to
RequestEncodingType.URL_ENCODED or
RequestEncodingType.MULTI_PART
from flask import Flask, request
app = Flask(__name__)
@app.post("/upload")
def upload():
f = request.files["data"]
return {"filename": f.filename}
from typing import Annotated
from litestar import Litestar, post
from litestar.datastructures import UploadFile
from litestar.enums import RequestEncodingType
from litestar.params import Body
@post("/upload")
async def upload(
data: Annotated[UploadFile, Body(media_type=RequestEncodingType.MULTI_PART)],
) -> dict[str, str | None]:
return {"filename": data.filename}
app = Litestar(route_handlers=[upload])
Producing responses#
Status codes#
Flask overrides the default 200 by returning a (body, status) tuple. Litestar
declares the status code on the decorator with status_code= when it is fixed for the
handler, and on the Response instance when it depends on the
request.
Litestar picks the status code from the HTTP method by default: POST defaults to
201 Created, DELETE to 204 No Content, everything else to 200 OK.
from flask import Flask
app = Flask(__name__)
@app.get("/")
def index():
return "not found", 404
from litestar import Litestar, Response, get
@get("/static", status_code=404, sync_to_thread=False)
def static_status() -> str:
return "not found"
@get("/dynamic", sync_to_thread=False)
def dynamic_status() -> Response[str]:
return Response("not found", status_code=404)
app = Litestar(route_handlers=[static_status, dynamic_status])
Serialization#
Flask mixes explicit jsonify calls with inference from the return value.
Litestar infers content encoding type from a handler’s return annotation. Structured
types (dict, list, dataclasses, Pydantic models, msgspec Structs, attrs
classes, typing.TypedDict, etc.) default to JSON, str returns
text/plain. You can set a media_type= on the handler decorator to override these
defaults.
from flask import Flask, Response
app = Flask(__name__)
@app.get("/json")
def get_json():
return {"hello": "world"}
@app.get("/text")
def get_text():
return "hello, world"
@app.get("/html")
def get_html():
return Response("<strong>hello, world</strong>", mimetype="text/html")
from litestar import Litestar, MediaType, get
@get("/json", sync_to_thread=False)
def get_json() -> dict[str, str]:
return {"hello": "world"}
@get("/text", media_type=MediaType.TEXT, sync_to_thread=False)
def get_text() -> str:
return "hello, world"
@get("/html", media_type=MediaType.HTML, sync_to_thread=False)
def get_html() -> str:
return "<strong>hello, world</strong>"
app = Litestar(route_handlers=[get_json, get_text, get_html])
See also
URL lookup#
Flask exposes url_for(endpoint, **params) as a module-level function.
Litestar exposes request.url_for on
the request instead. The endpoint name comes from an explicit name= keyword on the
handler decorator, the equivalent of Flask’s view-function name.
from flask import url_for
url_for("index")
from litestar import Litestar, Request, get
from litestar.response import Redirect
@get("/", name="index", sync_to_thread=False)
def index() -> str:
return "hello"
@get("/hello", sync_to_thread=False)
def hello(request: Request) -> Redirect:
return Redirect(path=request.url_for("index"))
app = Litestar(route_handlers=[index, hello])
Templates#
Flask ships Jinja2 and renders templates with render_template. Litestar treats
templating as opt-in: Jinja2 is available through the litestar[jinja] extra, Mako
through litestar[mako]. The engine is configured on the application through
TemplateConfig, and each handler returns a
Template — the equivalent of render_template’s return
value.
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/hello/<name>")
def hello(name):
return render_template("hello.html", name=name)
from pathlib import Path
from litestar import Litestar, get
from litestar.params import FromPath
from litestar.plugins.jinja import JinjaTemplateEngine
from litestar.response import Template
from litestar.template.config import TemplateConfig
@get("/hello/{name:str}", sync_to_thread=False)
def hello(name: FromPath[str]) -> Template:
return Template(
template_str="<p>Hello, {{ name }}!</p>",
context={"name": name},
)
app = Litestar(
route_handlers=[hello],
template_config=TemplateConfig(
directory=Path(__file__).parent,
engine=JinjaTemplateEngine,
),
)
See also
Streaming responses#
Flask streams by returning a generator wrapped in flask.Response. In Litestar, you
can return a Stream built from a sync or async iterator.
from flask import Flask, Response
app = Flask(__name__)
@app.get("/stream")
def stream():
def gen():
for i in range(3):
yield f"data: {i}\n\n"
return Response(gen(), mimetype="text/event-stream")
from collections.abc import AsyncIterator
from litestar import Litestar, get
from litestar.response import Stream
async def event_source() -> AsyncIterator[bytes]:
for i in range(3):
yield f"data: {i}\n\n".encode()
@get("/stream")
async def stream() -> Stream:
return Stream(event_source(), media_type="text/event-stream")
app = Litestar(route_handlers=[stream])
Exceptions and error responses#
Flask aborts with abort(code, description). Litestar raises an
HTTPException (or a subclass) directly.
from flask import Flask, abort
app = Flask(__name__)
@app.get("/")
def index():
abort(400, "this did not work")
from litestar import Litestar, get
from litestar.exceptions import HTTPException
@get("/", sync_to_thread=False)
def index() -> None:
raise HTTPException(status_code=400, detail="this did not work")
app = Litestar(route_handlers=[index])
See also
Custom exception handlers#
Flask registers handlers through @app.errorhandler. Litestar accepts a mapping from
an exception class or status code to a handler callable.
Tip
Exception handling is part of Litestar’s layered architecture, so these can be declared on applications, routers, controllers or route handlers.
from flask import Flask
from werkzeug.exceptions import HTTPException
app = Flask(__name__)
@app.errorhandler(HTTPException)
def handle_exception(e):
return {"detail": e.description}, e.code
from litestar import Litestar, Request, Response, get
from litestar.exceptions import HTTPException
def handle_http_exception(request: Request, exception: HTTPException) -> Response[dict[str, str]]:
return Response(
{"detail": exception.detail},
status_code=exception.status_code,
)
@get("/", sync_to_thread=False)
def index() -> None:
raise HTTPException(status_code=400, detail="this did not work")
app = Litestar(
route_handlers=[index],
exception_handlers={HTTPException: handle_http_exception},
)
See also
Before and after request hooks#
Flask uses @app.before_request and @app.after_request. Litestar accepts the same
callables through the before_request and after_request keyword arguments on any
layer.
Tip
Lifecycle hooks are part of Litestar’s layered architecture, so these can be declared on applications, routers, controllers or route handlers
from flask import Flask, g
app = Flask(__name__)
@app.before_request
def attach_user() -> None:
g.user = "alice"
@app.after_request
def wrap_text(response):
if response.mimetype == "text/plain":
return {"value": response.get_data(as_text=True)}
return response
from litestar import Litestar, MediaType, Request, Response, get
def attach_user(request: Request) -> None:
request.state["user"] = "alice"
def wrap_text_responses(response: Response) -> Response:
if response.media_type == MediaType.TEXT:
return Response({"value": response.content})
return response
@get("/hello", sync_to_thread=False)
def hello(request: Request) -> str:
return f"hello, {request.state['user']}"
app = Litestar(
route_handlers=[hello],
before_request=attach_user,
after_request=wrap_text_responses,
)
See also
Sessions#
Flask exposes flask.session as a thread-local dictionary backed by a signed cookie.
Litestar provides sessions via a session middleware, and exposes request.session as
a mutable mapping. Two backends are available:
ServerSideSessionConfig keeps the data
on the server and a session id in the cookie;
CookieBackendConfig
stores encrypted data in the cookie itself (closest to Flask’s default).
from flask import Flask, session
app = Flask(__name__)
app.secret_key = "..."
@app.post("/login")
def login():
session["user"] = "alice"
return ""
@app.get("/whoami")
def whoami():
return {"user": session.get("user")}
from typing import Any
from litestar import Litestar, Request, get, post
from litestar.middleware.session.server_side import ServerSideSessionConfig
session_config = ServerSideSessionConfig()
@post("/login", sync_to_thread=False)
def login(request: Request) -> None:
request.set_session({"user": "alice"})
@get("/whoami", sync_to_thread=False)
def whoami(request: Request) -> dict[str, Any]:
return {"user": request.session.get("user")}
app = Litestar(
route_handlers=[login, whoami],
middleware=[session_config.middleware],
)
See also
Static files#
Flask serves files from a folder named static automatically. Litestar offers an
equivalent in create_static_files_router(), which can be
registered to serve files from a configured directory:
from pathlib import Path
from litestar import Litestar
from litestar.static_files import create_static_files_router
ASSETS_DIR = Path(__file__).parent / "assets"
def ensure_assets() -> None:
ASSETS_DIR.mkdir(exist_ok=True)
ASSETS_DIR.joinpath("hello.txt").write_text("Hello, world!")
app = Litestar(
route_handlers=[
create_static_files_router(path="/static", directories=[ASSETS_DIR]),
],
on_startup=[ensure_assets],
)
See also
WebSockets#
Flask does not include WebSocket support natively, but flask-sock` and
flask-socketio are popular extensions from the ecosystem. Litestar does include
native WebSocket support, coming in three different shapes:
websocket(): direct connection handling via raw ASGI WebSocketswebsocket_listener(): Per-message callback style that takes and returns typed values; the framework handles accept, the receive loop, and serialisation.websocket_stream(): Async generator to produce messages that are pushed to the WebSocket; the framework handles accept, the receive loop, and serialisation.
from litestar import Litestar, websocket_listener
@websocket_listener("/ws")
async def chat(data: str) -> str:
return f"you said: {data}"
app = Litestar(route_handlers=[chat])
For broadcast and pub/sub patterns, pair these handlers with the channels plugin. Channels handles per-channel subscriptions, history, and inter-process fan-out through a pluggable broker, and can generate WebSocket route handlers that publish incoming events to subscribed clients.
See also
Testing#
Flask exposes app.test_client(). Litestar offers
TestClient (which wraps an existing app) and
create_test_client() (which builds a fresh app from the same
options as Litestar).
def test_index() -> None:
client = app.test_client()
response = client.get("/")
assert response.status_code == 200
assert response.text == "Index Page"
from litestar import get
from litestar.status_codes import HTTP_200_OK
from litestar.testing import create_test_client
@get("/", sync_to_thread=False)
def index() -> str:
return "Index Page"
def test_index() -> None:
with create_test_client(route_handlers=[index]) as client:
response = client.get("/")
assert response.status_code == HTTP_200_OK
assert response.text == "Index Page"
See also
Further reading#
These are not the only differences between Flask and Litestar, so if you want to keep reading about features, here’s a select list:
Quick reference#
A lookup table for the most common translations.
Concept |
Flask |
Litestar |
|---|---|---|
Route declaration |
|
|
Path parameter |
|
|
Request access |
global |
|
Request-scoped state |
|
|
Current application |
|
|
Static files |
automatic |
|
Templates |
|
|
Cookies / headers |
|
|
Redirect |
|
|
URL generation |
|
|
HTTPException |
|
|
Status code |
|
|
Serialization |
|
return type annotation |
Exception handler |
|
|
Before/after request |
|
|
Sessions |
|
|
File upload |
|
|
Streaming response |
|
|
WebSocket |
not built-in |
|
Lifespan |
|
|
Test client |
|
|