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

  1. Application (Litestar)

  2. Router / Controller (these are on the same level and can be arbitrarily nested)

  3. 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.

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

request.args

Injected via function parameters marked with FromQuery. Direct access through request.query_params

request.base_url

request.base_url

request.authorization

request.auth

request.cache_control

request.headers.get("cache-control")

request.content_encoding

request.headers.get("content-encoding")

request.content_length

request.headers.get("content-length")

request.content_type

request.content_type

request.cookies

request.cookies

request.data

await request.body()

request.date

request.headers.get("date")

request.endpoint

request.route_handler.name

request.environ

request.scope

request.files

Injected via UploadFile; see Content-type

request.form

Injected via the data parameter, marked with Body, see Content-type

request.get_json

Injected via the data parameter

request.headers

request.headers

request.host

request.if_match

request.headers.get("if-match")

request.if_modified_since

request.headers.get("if-modified-since")

request.if_none_match

request.headers.get("if-none-match")

request.if_range

request.headers.get("if-range")

request.if_unmodified_since

request.headers.get("if-unmodified-since")

request.method

request.method

request.path

request.scope["path"]

request.query_string

request.scope["query_string"]

request.range

request.headers.get("range")

request.referer

request.headers.get("referer")

request.remote_addr

request.client.host / request.client.port

request.root_path

request.scope["root_path"]

request.server

request.scope["server"]

request.url

request.url

request.user_agent

request.headers.get("user-agent")

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])

Cookies and headers#

Flask builds a response with make_response and mutates it. Litestar offers two paths: declare static values on the decorator with response_cookies and response_headers, or return a Response with cookies= and headers= when the values depend on the request.

from flask import Flask, make_response

app = Flask(__name__)

@app.get("/")
def index():
    response = make_response("hello")
    response.set_cookie("my-cookie", "cookie-value")
    response.headers["my-header"] = "header-value"
    return response
from litestar import Litestar, Response, get
from litestar.datastructures import Cookie


@get(
    "/static",
    response_headers={"my-header": "header-value"},
    response_cookies=[Cookie(key="my-cookie", value="cookie-value")],
    sync_to_thread=False,
)
def static() -> str:
    return "hello"


@get("/dynamic", sync_to_thread=False)
def dynamic() -> Response[str]:
    return Response(
        "hello",
        headers={"my-header": "header-value"},
        cookies=[Cookie(key="my-cookie", value="cookie-value")],
    )


app = Litestar(route_handlers=[static, dynamic])

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

Media Type

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])

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},
)

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],
)

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 WebSockets

  • websocket_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

@app.route("/")

@get("/") + Litestar([handler])

Path parameter

/items/<int:id>

/items/{id:int} + FromPath[int]

Request access

global request

request: Request parameter

Request-scoped state

flask.g

request.state

Current application

flask.current_app

request.app

Static files

automatic static/

create_static_files_router

Templates

render_template

Template() + TemplateConfig

Cookies / headers

make_response + .set_cookie

response_cookies= / Response(...)

Redirect

redirect + url_for

Redirect + request.url_for

URL generation

url_for

request.url_for(handler_name)

HTTPException

abort(400, ...)

HTTPException(status_code=400, ...)

Status code

return body, status

status_code= / Response(...)

Serialization

jsonify / inference

return type annotation

Exception handler

@app.errorhandler

exception_handlers={Exc: handler}

Before/after request

@before_request / @after_*

before_request= / after_request=

Sessions

flask.session

ServerSideSessionConfig

File upload

request.files

UploadFile + Body(MULTI_PART)

Streaming response

Response(generator, ...)

Stream(iterator)

WebSocket

not built-in

@websocket("/ws")

Lifespan

@before_first_request

lifespan=[ctx_mgr]

Test client

app.test_client()

TestClient / create_test_client