See what's new in Expanse

The limitless Python web framework

Expanse gives you the tools to build high-performance, scalable web applications with ease. Start building in minutes, keep using it for years.

Routing

Easy route definition with a touch of magic

Whether you need API endpoints or more standard web pages, Expanse got you covered. Use plain functions to handle your requests — or go further with controllers — and leverage the powerful dependency injection mechanism at the heart of Expanse to make even complex requirements a breeze.

Learn more
@group("docs", prefix="/docs")
class DocumentationController:

    def __init__(self, docs: Documentation) -> None:
        # A Documentation instance will automatically
        # be injected when instantiating the controller

    @get("/{page}", name="page")
    def show_page(
        self, request: Request, page: str
    ) -> Response:
        # The current request will be injected automatically
from expanse.contracts.routing.registrar import Registrar

from app.http.controllers.documentation import DocumentationController


def routes(router: Registrar) -> None:
    router.controller(DocumentationController)
{% for user in users %}
<a href="{{ route("users.show", {"user_id", user.id} }}">
    User {{ user.name }}
</a>
{% endfor %}
from expanse.view.view_factory import ViewFactory


def list_users(view: ViewFactory):
    users = ...

    return view.make("users.list", {"users": users})
from expanse.common.http.form import Form
from expanse.http.response import Response

from app.http.request.models.forms.article import ArticleModel


def create_article(article: Form[ArticleModel]) -> Response:
    if form.is_submitted() and form.is_valid():
        # Save article
        ...
from pydantic import BaseModel


class ArticleModel(BaseModel):

    title: str
    content: str
Data access

Powerful database management made easy

Expanse relies on SQLAlchemy to provide a powerful, yet intuitive, database experience. Its integration is seamless: Raw queries, ORM and migrations, they are all readily available, so that you can focus on what matters.

Learn more

Connecting to and using a database should be easy. Expanse manages your database connections automatically, so you can focus on retrieving and storing data. Declare as many as you need, then type-hint the one you want — reading from SQLite while writing to PostgreSQL costs you nothing but a line of configuration.

Thanks to the model-as-dataclasses approach, you models are lean and easy to understand. Reuse type definition for even simpler models. Serializing your models is also a breeze: define a serialization schema, annotate your return type with it and let Expanse do the rest.

Thanks to Alembic, preconfigured to suit most needs, versioning your schema has never been easier. Generate a migration from the changes you made to your models, review it, and apply it.

from typing import Annotated

from expanse.database.connection import Connection
from expanse.routing.helpers import get
from expanse.view.view import View


@get("/articles/{article_id}")
def show_article(
    connection: Connection,
    analytics: Annotated[Connection, "analytics"],
    article_id: int,
) -> View:
    article = connection.execute(
        "SELECT * FROM articles WHERE id = :id",
        {"id": article_id},
    )

    analytics.execute(
        "INSERT INTO article_views (article_id) VALUES (:id)",
        {"id": article_id},
    )
from expanse.routing.router import Registrar

from app.http.controllers.articles import show_article


def routes(router: Registrar) -> None:
    router.handler(show_article)
from expanse.database.orm import column
from sqlalchemy.orm import Mapped

from app.models.model import Model, primary_key


class User(Model):
    __tablename__: str = "users"

    id: Mapped[primary_key] = column()
    first_name: Mapped[str] = column()
    last_name: Mapped[str | None] = column(default=None)
    email: Mapped[str] = column()
from typing import Annotated

from expanse.database.orm import column
from expanse.database.orm.model import Model
from sqlalchemy import BigInteger, Identity
from sqlalchemy.dialects import sqlite


# Declared once, reused by every model that needs it.
primary_key = Annotated[
    int,
    column(
        BigInteger().with_variant(sqlite.INTEGER(), "sqlite"),
        Identity(always=True),
        primary_key=True,
    ),
]
from collections.abc import Sequence
from typing import Annotated

from expanse.database.session import Session
from expanse.routing.helpers import get
from sqlalchemy import select

from app.models.user import User
from app.schemas.user import UserData


@get("/users")
def list_users(session: Session) -> Sequence[Annotated[User, UserData]]:
    return session.scalars(select(User)).all()
                
                  
                    $
                    ./beam make migration "Create the posts table" --auto
                  
                   
                  
                      - Generating
                    
                      database/migrations/versions/2026_08_29_143512_e776feeeb86d_create_the_posts_table.py... Done
                  
                   
                  
                    $
                    ./beam db migrate
                  
                   
                  
                
                    -
                  Applying migration
                  a4f1c9d02b73
                  (Create the users table)
                
              
                
                    -
                  Applying migration
                  7c5b18ee4a90
                  (Add an index on emails)
                
              
                
                    -
                  Applying migration
                  3d0ba62f14c8
                  (Create the tags table)
                
              
                
                    -
                  Applying migration
                  e776feeeb86d
                  (Create the posts table)
                
              
                
              
"""
Create the posts table

Revision ID: e776feeeb86d
Revises:
Create Date: 2026-08-29 14:35:12.933029
"""
from alembic import op
import sqlalchemy as sa

revision: str = "e776feeeb86d"
down_revision: str | None = None


def upgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table(
        "posts",
        sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
        sa.Column("title", sa.String(), nullable=False),
        sa.Column("content", sa.Text(), nullable=False),
        sa.PrimaryKeyConstraint("id"),
    )
    # ### end Alembic commands ###


def downgrade() -> None:
    op.drop_table("posts")
Background jobs

Move slow work
off the request.

Improve the responsiveness of your application by deferring heavy tasks. Expanse has a comprehensive jobs and queue system, backed by a standalone message bus: delays, retries and failure tracking are supported out of the box.

One dispatch, five stops

From dispatch to processing, your messages are handled reliably by the framework at each step. Easily switch transports when needed, and add your own middleware to extend the capabilities of the message bus.

Dispatch jobs and move on

Background jobs are just messages at heart, with syntactic sugar on top to make them easier to define and dispatch. Create a job class, implement the execute() method. It gets the same dependency injection as your route handlers, so ask for what you need and let a worker do the rest.

Tune it per job
Everything you need

Batteries included, and durable.

Encryption, caching, API documentation and more: everything you need to build great applications is readily available and just a few environment variables away.

Native encryption

Protect sensitive information with a secure encryption component, using proven standards.

Multi-tier caching

Speed up your application by leveraging a robust cache system, with an in-memory first tier for that extra boost in performance.

OpenAPI documentation

Focus on writing your API endpoints, and let Expanse generate the OpenAPI documentation for you, by analyzing your code and extracting the relevant information.

Unified storage

One API, multiple storage backends. Swap backends with an environment variable without touching your code.