Web frameworks

chameleon-rs works with the Python web frameworks you’re likely to use, with nothing extra to install. Each integration is a module of chameleon-rs that imports its framework only when you import it, and uses the version your app already has.

Framework Use It replaces Guide
Flask, Quart chameleon_rs.flask chameleon-flask Flask and Quart
FastAPI chameleon_rs.fastapi fastapi-chameleon FastAPI
Starlette chameleon_rs.starlette Starlette’s Jinja2Templates Starlette
Robyn chameleon_rs.robyn chameleon-robyn Robyn
Litestar chameleon_rs.litestar Litestar’s Jinja engine, litestar-chameleon Litestar
Pyramid pyramid_chameleon, unchanged (runs through compat.install()) Pyramid

Already using one of the libraries in the third column? chameleon_rs.compat.install() makes its import give the built-in, so the app runs unchanged. See Switching from Chameleon.

The same page in each framework

Each example renders templates/home/index.pt:

<h1>${title}</h1>
<ul><li tal:repeat="v videos">${v}</li></ul>
import flask

import chameleon_rs.flask as chameleon_flask

app = flask.Flask(__name__)
chameleon_flask.configure(template_folder='templates', app=app)


@app.get('/')
@chameleon_flask.template('home/index.pt')
def index():
    return {'title': 'Latest videos', 'videos': ['intro', 'partials']}
import quart

import chameleon_rs.flask as chameleon_flask

app = quart.Quart(__name__)
chameleon_flask.configure(template_folder='templates', app=app)


@app.get('/')
@chameleon_flask.template('home/index.pt')
async def index():
    return {'title': 'Latest videos', 'videos': await load_videos()}
import fastapi

import chameleon_rs.fastapi as fastapi_chameleon

app = fastapi.FastAPI()
fastapi_chameleon.configure(template_folder='templates', app=app)


@app.get('/')
@fastapi_chameleon.template('home/index.pt')
async def index():
    return {'title': 'Latest videos', 'videos': await load_videos()}
from starlette.applications import Starlette
from starlette.routing import Route

from chameleon_rs.starlette import Templates

templates = Templates(directory='templates')


async def index(request):
    model = {'title': 'Latest videos', 'videos': await load_videos()}
    return await templates.TemplateResponse_async(request, 'home/index.pt', model)


app = Starlette(routes=[Route('/', index)])
from robyn import Robyn

import chameleon_rs.robyn as chameleon_robyn

app = Robyn(__file__)
chameleon_robyn.configure(template_folder='templates', app=app)


@app.get('/')
@chameleon_robyn.template('home/index.pt')
async def index():
    return {'title': 'Latest videos', 'videos': await load_videos()}
from litestar import Litestar, get

import chameleon_rs.litestar as chameleon_litestar


@get('/')
@chameleon_litestar.template('home/index.pt')
async def index() -> dict:
    return {'title': 'Latest videos', 'videos': await load_videos()}


app = Litestar([index])
chameleon_litestar.configure(template_folder='templates', app=app)
import chameleon_rs.compat

chameleon_rs.compat.install()   # first, before pyramid_chameleon is imported

from pyramid.config import Configurator
from pyramid.view import view_config


@view_config(route_name='home', renderer='templates/home/index.pt')
def index(request):
    return {'title': 'Latest videos', 'videos': ['intro', 'partials']}


def main(global_config, **settings):
    with Configurator(settings=settings) as config:
        config.include('pyramid_chameleon')
        config.add_route('home', '/')
        config.scan()
    return config.make_wsgi_app()

What the integrations share

Flask, FastAPI, Starlette, Robyn and Litestar have the same decorator API, so what you learn on one carries over:

  • configure(template_folder=..., auto_reload=..., app=app) is the same call as chameleon_rs.configure(): one setup for views, partials and Markdown. Every call applies. global_init(...) is the original libraries’ name and still works.
  • @template('page.pt') renders what the view returns: a dict, any mapping, a dataclass or a Pydantic model. A framework response passes through unchanged, for redirects. status_code= and content_type= set the response; bare @template picks {module}/{function}.html or .pt.
  • not_found() and error(template_file, status_code=500) end a view with an error page.
  • render() and response(), with render_async() and response_async(), render without the decorator: an email body, a feed, an HTMX fragment.
  • check_views(app) and clear() for tests.
  • Errors share one base class, chameleon_rs.web.TemplateError, whichever module raised them.

Async views

An async def view renders with render_async: a model value that is awaitable is awaited where the template uses it, and the render doesn’t block the event loop while it waits. A plain def view renders with render(), where the framework runs it. See Async rendering for the details and the opt-in concurrency settings.

What a template gets

Integration Besides the model
Flask, Quart, FastAPI, Robyn Nothing: the template gets what the view returns, as with the original libraries. Return request in the model if the template needs it.
Starlette request and url_for, as with Jinja2Templates, plus its context processors with Templates.
Litestar request, csrf_input, url_for, csrf_token, url_for_static_asset and your registered template callables.
Pyramid request, context, view and renderer_name.

Every template also has render_partial built in, and markdown: once it’s turned on.

Decorator order

@template goes below the route decorator:

@app.get('/')                         # first: the route...
@chameleon_flask.template('page.pt')  # ...then @template
def index(): ...

Written the other way round, the router registers the bare view, the template never renders and the route quietly returns the view’s dict as JSON. Pass app= to configure() and the mistake fails loudly with a DecoratorOrderError that names the view and the fix: when the app starts (FastAPI, Starlette, Quart, Robyn, Litestar) or on the first request (Flask, which has no startup hook). Each guide has the details.

WarningTemplate names come from your code

The names given to @template, render(), response(), the error pages and render_partial() are looked up as the original libraries look them up, which allows .. and absolute paths. Write them in your code; never build one from a request (a path or query parameter, a header). Starlette’s Templates and Litestar’s ChameleonEngine refuse names that reach outside their folder, as Jinja does.