The limitless Python web framework

Expanse gives you the tools to build high-performance, scalable web applications with ease. Start building in minutes, stay 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
from expanse.http.response import Response
from expanse.routing.router import Router

from app.documentation import Documentation


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

Dependency injection

At the heart of Expanse is a powerful — yet intuitive — dependency injection mechanism that orchestrates every service that your application might need.

Type-hint any dependencies in your route functions or controllers, and they will be automatically injected for you.

In most cases you will be able to benefit from dependency injection for your own classes without doing anything.

{% 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})

Composable views

Easily render and compose views. Expanse is powered by Jinja meaning that you have natively access to all the features it provides.

Expanse provides additional features and helpers to make common actions more intuitive. And if it's not enough you can easily add your own, if needed.

Intuitive validation

Need to validate a form, a JSON payload or query parameters? Just specify a validation model and let Expanse handle the validation and report the errors

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
from expanse.common.http.json import JSON

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


def create_article(article_data: JSON[ArticleModel]):
    # `article_data` is a fully validated instance of ArticleModel
    article = ArticleModel(**article_data.model_dump())
from pydantic import BaseModel


class ArticleModel(BaseModel):

    title: str
    content: str
from expanse.http.query import Query

from app.http.request.models.queries.article import ListArticleModel


def list_articles(session: Session, q: Query[ListArticleModel]):
    # `q` is a fully validated instance of ArticleModel
    articles = session.scalars(
        select(Article).where(Article.category == q.category)
    ).all()
from pydantic import BaseModel


class ListArticleModel(BaseModel):

    category: str
    before: datetime
POST https://example.com/articles HTTP/1.1
Content-Type: application/json

{"content": "Content"}

                    
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json


{
  "code": "validation_error",
  "detail": [
    {
      "loc": ["title"],
      "message": "Field required",
      "type": "missing",
    }
  ]
}
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
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 list_articles(
    connection: Connection,
    analytics_connection: Annotated[Connection, "analytics"],
    article_id: int,
) -> View:
    article = connection.execute("SELECT * FROM articles WHERE id = :id", {"id": article_id})

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

from app.http.controllers.articles import list_articles


def routes(router: Registrar) -> None:
    router.handler(list_articles)

Automatic connection management

Connecting to and using a database should be easy. Expanse manages your database connections automatically, so you can focus on retrieving and storing data.

Multiple databases support

You can configure as many databases as you want. Need to read from an SQLite database and write to a PostgreSQL one? No problem.

from expanse.database.orm.model import Model


primary_key = Annotated[
    int,
    column(
        BigInteger().with_variant(sqlite.INTEGER(), "sqlite"),
        Identity(always=True),
        primary_key=True,
    ),
]

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 collections.abc import Sequence

from expanse.database.session import Session


@get("/users")
def list_users(session: Session) -> Sequence[Annotated[User, UserData]]:
    users = session.scalars(select(User)).all()

    return users

Intuitive models

Thanks to the model-as-dataclasses approach, you models are lean and easy to understand. Reuse type definition for even simpler models.

Built-in serialization

Serializing your models is also a breeze: define a serialization schema, annotate your return type with it and let Expanse do the rest.

"""
from alembic import op
import sqlalchemy as sa


def upgrade() -> None:
    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"),
    )


def downgrade() -> None:
    op.drop_table("posts")
$ ./beam make migration create_users_table

$ ./beam db migrate
Core features

What's inside Expanse?

Native encryption

Keep your data safe with built-in encryption using proven standards.

Middleware

Intuitive middleware with dependency injection support.

Exception management

Easily spot errors with customizable reporting and a beautiful error page.