Zum Inhalt springen
Michael Blaess
IT-Consulting
Michael Blaess
DE

A web interface without a JavaScript project? FastHTML in the Python stack

PythonFastHTMLHTMXWeb

Building a web interface for a small internal tool has become surprisingly expensive. You create a second project, pick a frontend framework, set up a build step and maintain dependencies in a second language. For a dashboard that shows three numbers from a database, that is out of all proportion.

FastHTML by Answer.AI takes the other route: the interface is written in Python, HTML is produced by calling functions, and interactivity comes from HTMX. I used it properly for the first time for a tool of my own.

What it feels like

A complete application looks like this:

from fasthtml.common import *

app, rt = fast_app()

@rt
def index():
    return Titled("Home", P("Hello world"))

serve()

That starts a server on port 5001. No project scaffold, no configuration file, no build.

HTML elements are Python functions: Div, H1, P, Button. Attributes become arguments. cls stands in for the reserved word class, and a _ in an attribute name turns into a -, so hx_get becomes hx-get.

The real appeal is somewhere else. HTMX is a small JavaScript library that lets an element call a server through an HTML attribute and swap part of the page with the answer. The server sends back HTML, not JSON. That removes the whole translation layer where you would otherwise serialise data, read it back in the browser and turn it into HTML all over again.

In FastHTML you hand the button the function that produces the fragment:

@rt
def step_detail(key: str):
    return Div(..., id="detailbox")

Button("Details",
    hx_get=step_detail.to(key="durable"),
    hx_target="#detailbox",
    hx_swap="outerHTML")

The .to() builds the path including parameters out of the function itself. You never type a route as a string, and renaming does not break anything silently.

Another handy detail: the same route serves both cases. On a normal request FastHTML wraps the return value into a full page, on an HTMX request only the fragment comes back. You write the view once.

Where FastHTML does not belong

This became the most important point while trying it out.

FastHTML is a server framework. A running app needs a running process, and every HTMX interaction is a call to the server. The tool choice follows almost by itself:

  • A good fit when a Python backend genuinely earns its keep: querying a SQLite database or an API live, holding state between requests, server-side logic. Or when an existing terminal application should get a web counterpart on top of the same core.
  • A bad fit for a diagram that goes into a slide deck as a screenshot, has to work offline, or gets passed around as a single file. There a server only adds a dependency without contributing anything. Expanding, toggling, tabs and other purely click-driven interactivity need no server at all.

The rule of thumb it boils down to:

A server call needed or a Python backend in play, then FastHTML. Otherwise a file.

And if it does have to be a file

The two go together. You can build a FastHTML app, run it comfortably in the browser while developing, and export one self-contained HTML file from it at the end. Double-clickable, offline, sendable by mail.

FastHTML has no built-in function for that, but the recipe is manageable:

  1. Render every fragment up front. What would otherwise arrive from the server on click is turned into HTML strings with to_xml() at export time and placed into the page as JSON inside a <script>.
  2. Swap HTMX for data attributes. An hx-get becomes a data-detail, and a single click handler swaps in the matching entry from the embedded store. No HTMX, no server, just a little JavaScript right there in the file.
  3. Write the whole page, again through to_xml(), with <!doctype html> in front.
  4. Compress: to_xml(..., indent=False) gives you the page without line breaks, and the embedded CSS collapses to one line with a re.sub(r"\s+", " ", CSS).

For the file to really work offline, the app has to do without foreign sources from the start: CSS embedded rather than loaded over a network, no external typeface, the bundled default stylesheet switched off with pico=False. Stick to system fonts and every machine shows the same picture.

The switch for it fits on one line:

if "--export" in sys.argv:
    build_static()
else:
    serve()

I went through this with an interactive document built around a timeline, with clickable detail cards and a toggle. The result: one file without a single external reference. Whoever receives it needs neither Python nor a web server, only a browser. For handing something to people without a development environment, that is the decisive difference.

But watch out: because every detail card ends up pre-rendered inside the file, it grows with the amount of content. With a lot of material it would probably be cheaper to embed the data as JSON instead of finished HTML and render it in the browser.

Traps that cost time

Checked against fasthtml 0.14.13, as of September 2026. Four things you do not see coming the first time:

  • The SVG shapes live elsewhere. Line, Circle and friends are missing from fasthtml.common, they come from fasthtml.svg. Take Svg from there as well, because only that version sets the SVG namespace. Miss the import and the server starts cleanly, then throws the NameError on the first request to the page.
  • Titled("") produces an empty heading. Titled renders its title as an <h1>. If you design your own header, return a plain Div instead and set the page title through fast_app(title=...).
  • Restarting is not reloading. serve() restarts the server on code changes by itself, but the browser only reloads the page automatically with fast_app(live=True). On top of that, an app I had started detached in the background did not pick up my edits at all. You then debug against old code and read line numbers that no longer match 😕 Only a clean restart helps.
  • serve() listens on every network address, not just the local machine. If you do not want that, set serve(host="127.0.0.1") explicitly.

The first trap shows a pattern worth remembering:

Errors show up at request time, not at start-up.

A running server proves nothing. Call every route individually, the fragments included, because a green landing page does not mean every detail renders.

Where you can run it

Underneath FastHTML runs Uvicorn, an ordinary Python web server. So anything that can execute a Python process will do.

Answer.AI has put together example projects for nine platforms: Hugging Face Spaces, Railway, Replit, Vercel, Heroku, Fly.io, Coolify, Modal and a DigitalOcean server. The repository has not been updated since September 2024, though, so check the instructions against the current documentation of the platform in question.

For your own machine, Answer.AI recommends nginx or Caddy as the front server. Not Apache, because it does not support ASGI, the asynchronous Python server interface Uvicorn is built on.

The flip side belongs in the picture: a purely static web space cannot serve such an app. This site here, for example, consists of pre-built files, there is no Python process for a FastHTML app to hook into. If you have nowhere to keep a process running, you end up back at the single-file export - and find that for many cases it is entirely enough.

Sources (checked on 15.09.2026): FastHTML · deployment examples by Answer.AI · HTMX

“Our life is what our thoughts make it.”

- Marc Aurel