import fastapi
import chameleon_rs.fastapi as fastapi_chameleon
app = fastapi.FastAPI()
fastapi_chameleon.configure(template_folder=template_folder, app=app)
EPISODES = ['Python 3.14', 'Rust for Pythonistas', 'HTMX in practice']
async def find(q: str) -> list[str]:
return [e for e in EPISODES if q.lower() in e.lower()]
def wants_fragment(request: fastapi.Request) -> bool:
headers = request.headers
return 'HX-Request' in headers and 'HX-History-Restore-Request' not in headers
@app.get('/search')
async def search(request: fastapi.Request, q: str = ''):
results = await find(q)
if wants_fragment(request):
return await fastapi_chameleon.response_async('search/results.pt', results=results)
return await fastapi_chameleon.response_async('search/index.pt', q=q, results=results)HTMX
HTMX swaps part of a page with an HTML fragment the server returns. chameleon-rs has what that needs built in. Partials (chameleon_partials, built in) are the fragments, and every framework integration can return one on its own. There is nothing extra to install or register.
The pattern is the one chameleon_partials was written for. Put the piece of the page HTMX replaces in its own template. The full page includes it with render_partial, and the view HTMX calls renders only that template. Because both use the same file, the first page load and the HTMX response can’t drift apart.
One template, two uses
A search box that updates the results as you type. The fragment, search/results.pt:
<ul id="results">
<li tal:repeat="episode results">${episode}</li>
</ul>The page, search/index.pt, includes it:
<input type="search" name="q" value="${q}"
hx-get="/search" hx-trigger="input changed delay:300ms"
hx-target="#results" hx-swap="outerHTML" hx-push-url="true">
${render_partial('search/results.pt', results=results)}The partial renders as a native frame of the page (see Partials), so including it costs about the same as writing its markup inline.
Full page or fragment from one view
HTMX sends an HX-Request: true header with every request it makes. One view can check it, returning the fragment to HTMX and the whole page to everyone else: a first visit, a reload, or a shared link to a URL that hx-push-url put in the address bar. With FastAPI:
from fastapi.testclient import TestClient
client = TestClient(app)
print(client.get('/search', params={'q': 'py'}, headers={'HX-Request': 'true'}).text)<ul id="results">
<li>Python 3.14</li>
<li>Rust for Pythonistas</li>
</ul>
When the back button finds a page missing from its history cache, HTMX fetches it again with HX-History-Restore-Request: true as well and expects the whole page, which is why wants_fragment checks for that header too. If a cache sits in front of the app, send Vary: HX-Request so it doesn’t serve one response in place of the other.
Every integration has the same response() and response_async(): chameleon_rs.flask, chameleon_rs.starlette, chameleon_rs.robyn and chameleon_rs.litestar as well as chameleon_rs.fastapi.
A view that only returns the fragment
When a route exists only for HTMX, such as an hx-post that adds a row, put @template on the fragment itself:
@app.post('/todos') # the route decorator first...
@fastapi_chameleon.template('todos/list.pt') # ...then @template, naming the fragment
async def add(title: str = fastapi.Form()):
await db.add_todo(title)
return {'todos': await db.todos()}A partial is an ordinary template, so the decorators render it like any page, async views included: an async def view renders with render_async and doesn’t block the event loop (see Async rendering).
From Python, without a framework
chameleon_rs.render_partial(path, **model) renders a fragment anywhere: an HTMX view in a framework without an integration, a WebSocket or server-sent events handler pushing HTML, or a test. It returns an HTML object whose html_text is the markup:
import chameleon_rs
fragment = chameleon_rs.render_partial('search/results.pt', results=['HTMX in practice'])
print(fragment.html_text)<ul id="results">
<li>HTMX in practice</li>
</ul>
From Python the path is looked up in the configured template folder only, as with chameleon_partials. The HTMX fragments recipe shows the same idea with Flask.