Zum Inhalt springen
Michael Blaess
IT-Consulting
Michael Blaess
EN

Eine Weboberfläche ohne JavaScript-Projekt? FastHTML im Python-Stack

PythonFastHTMLHTMXWeb

Für ein kleines internes Werkzeug eine Weboberfläche zu bauen, ist erstaunlich aufwendig geworden. Man legt ein zweites Projekt an, wählt ein Frontend-Framework, richtet einen Build-Schritt ein und pflegt Abhängigkeiten in einer zweiten Sprache. Für ein Dashboard, das drei Zahlen aus einer Datenbank zeigt, steht das in keinem Verhältnis.

FastHTML von Answer.AI geht den anderen Weg: Die Oberfläche wird in Python geschrieben, HTML entsteht als Funktionsaufruf, und die Interaktivität kommt über HTMX. Ich habe es für ein eigenes Werkzeug das erste Mal ernsthaft benutzt.

Wie sich das anfühlt

Eine vollständige Anwendung sieht so aus:

from fasthtml.common import *

app, rt = fast_app()

@rt
def index():
    return Titled("Home", P("Hallo Welt"))

serve()

Das startet einen Server auf Port 5001. Kein Projektgerüst, keine Konfigurationsdatei, kein Build.

HTML-Elemente sind Python-Funktionen: Div, H1, P, Button. Attribute werden zu Argumenten. Dabei ersetzt cls das reservierte Wort class, und ein Unterstrich wird zum Bindestrich, aus hx_get also hx-get.

Der eigentliche Reiz liegt woanders. HTMX ist eine kleine JavaScript-Bibliothek, mit der ein Element per HTML-Attribut einen Server aufrufen und mit der Antwort einen Teil der Seite austauschen kann. Der Server schickt dabei HTML zurück, kein JSON. Damit fällt die ganze Übersetzungsschicht weg, in der man sonst Daten serialisiert, im Browser wieder einliest und dort erneut zu HTML macht.

In FastHTML übergibt man der Schaltfläche direkt die Funktion, die das Fragment liefert:

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

Das .to() baut den Pfad samt Parametern aus der Funktion selbst. Man tippt keine Route als Zeichenkette ab, und beim Umbenennen bricht nichts still.

Praktisch ist außerdem, dass dieselbe Route beides bedient: Bei einem normalen Aufruf verpackt FastHTML die Rückgabe automatisch in eine ganze Seite, bei einem HTMX-Aufruf kommt nur das Fragment. Man schreibt die Ansicht einmal.

Wo FastHTML nicht hingehört

Das ist der Punkt, der mir beim Ausprobieren am wichtigsten wurde.

FastHTML ist ein Server-Framework. Eine laufende App braucht einen laufenden Prozess, und jede HTMX-Interaktion ist ein Aufruf zum Server. Daraus folgt die Werkzeugwahl fast von allein:

  • Passend, wenn ein Python-Backend echten Mehrwert bringt: Daten aus einer SQLite-Datenbank oder einer Schnittstelle live abfragen, Zustand zwischen Aufrufen halten, serverseitige Logik. Oder wenn eine bestehende Terminal-Anwendung ein Web-Gegenstück über demselben Kern bekommen soll.
  • Unpassend für ein Diagramm, das als Screenshot in eine Präsentation soll, offline laufen muss oder als einzelne Datei weitergegeben wird. Dort fügt ein Server nur eine Abhängigkeit hinzu, ohne etwas beizutragen. Aufklappen, Umschalten, Reiter und andere reine Klick-Interaktivität brauchen keinen Server.

Die Faustregel, auf die es hinausläuft:

Server-Aufruf nötig oder Python-Backend im Spiel, dann FastHTML. Sonst eine Datei.

Und wenn es doch eine Datei sein soll

Beides geht zusammen. Man kann eine FastHTML-App bauen, sie beim Entwickeln bequem im Browser laufen lassen und am Ende eine einzige, in sich geschlossene HTML-Datei daraus exportieren. Doppelklickbar, offline, per Mail verschickbar.

Eine fertige Funktion dafür bringt FastHTML nicht mit, das Rezept ist aber überschaubar:

  1. Alle Fragmente vorher rendern. Was sonst auf Klick vom Server käme, wird beim Export mit to_xml() in HTML-Zeichenketten verwandelt und als JSON in ein <script> in die Seite gelegt.
  2. HTMX gegen Datenattribute tauschen. Aus hx-get wird ein data-detail, und ein einziger Klick-Handler tauscht den passenden Eintrag aus dem eingebetteten Vorrat ein. Kein HTMX, kein Server, nur etwas JavaScript direkt in der Datei.
  3. Die ganze Seite schreiben, ebenfalls über to_xml(), mit <!doctype html> davor.
  4. Verdichten: to_xml(..., indent=False) liefert die Seite ohne Umbrüche, und das eingebettete CSS lässt sich mit einem re.sub(r"\s+", " ", CSS) auf eine Zeile bringen.

Damit die Datei offline wirklich funktioniert, muss die App von Anfang an ohne fremde Quellen auskommen: CSS eingebettet statt über ein Netz nachgeladen, keine externe Schriftart, das mitgelieferte Standard-Stylesheet abgeschaltet über pico=False. Wer Systemschriften nimmt, bekommt auf jedem Rechner dasselbe Bild.

Der Schalter dafür passt in eine Zeile:

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

Durchgespielt habe ich das an einem interaktiven Dokument mit einer Zeitleiste, anklickbaren Detailkarten und einem Umschalter. Ergebnis: eine Datei ohne einen einzigen externen Verweis. Wer sie bekommt, braucht weder Python noch einen Webserver, sondern nur einen Browser. Für die Weitergabe an Leute ohne Entwicklungsumgebung ist das der entscheidende Unterschied.

Aber Achtung: Weil beim Export jede Detailkarte vorgerendert in der Datei landet, wächst sie mit der Menge an Inhalt. Bei sehr viel Material dürfte es günstiger sein, statt fertigem HTML nur die Daten als JSON einzubetten und im Browser zu rendern.

Fallen, die Zeit gekostet haben

Geprüft gegen fasthtml 0.14.13, Stand September 2026. Vier Dinge, die man beim ersten Mal nicht ahnt:

  • Die SVG-Formen liegen woanders. Line, Circle und Co. fehlen in fasthtml.common, sie kommen aus fasthtml.svg. Auch Svg sollte man von dort nehmen, denn nur diese Fassung setzt den SVG-Namensraum. Fehlt der Import, startet der Server sauber und wirft den NameError erst beim ersten Aufruf der Seite.
  • Titled("") erzeugt eine leere Überschrift. Titled rendert seinen Titel als <h1> mit. Wer eine eigene Kopfzeile gestaltet, gibt besser ein schlichtes Div zurück und setzt den Seitentitel über fast_app(title=...).
  • Neustart ist nicht gleich Neuladen. serve() startet den Server bei Codeänderungen von selbst neu, der Browser lädt die Seite aber nur mit fast_app(live=True) automatisch nach. Bei mir kam dazu, dass eine abgekoppelt im Hintergrund gestartete App meine Änderungen gar nicht übernommen hat. Man debuggt dann gegen alten Code und liest Zeilennummern, die nicht mehr stimmen 😕 In dem Fall hilft nur ein sauberer Neustart.
  • serve() lauscht auf allen Netzwerkadressen, nicht nur auf dem eigenen Rechner. Wer das nicht will, setzt serve(host="127.0.0.1") ausdrücklich.

Die erste Falle zeigt ein Muster, das man sich merken sollte:

Fehler zeigen sich zur Aufrufzeit, nicht beim Start.

Ein laufender Server beweist nichts. Man sollte jede Route einzeln aufrufen, auch die Fragmente, denn eine grüne Startseite heißt nicht, dass jedes Detail rendert.

Wo man es betreiben kann

Unter FastHTML läuft Uvicorn, ein gewöhnlicher Python-Webserver. Damit ist alles möglich, was einen Python-Prozess ausführen kann.

Answer.AI hat Beispielprojekte für neun Plattformen zusammengestellt: Hugging Face Spaces, Railway, Replit, Vercel, Heroku, Fly.io, Coolify, Modal und einen DigitalOcean-Server. Das Repository wird allerdings seit September 2024 nicht mehr aktualisiert, die Anleitungen sollte man also gegen die aktuelle Doku der jeweiligen Plattform halten.

Für den eigenen Server empfiehlt Answer.AI nginx oder Caddy als vorgelagerten Webserver. Den Apache nicht, weil er die asynchrone Python-Schnittstelle ASGI nicht unterstützt, auf der Uvicorn aufbaut.

Die Kehrseite gehört dazu: Ein rein statischer Webspace kann so eine App nicht ausliefern. Diese Seite hier zum Beispiel besteht aus vorgebauten Dateien, dort läuft kein Python-Prozess, in den sich eine FastHTML-App einklinken könnte. Wer keinen Ort für einen dauerhaft laufenden Prozess hat, landet wieder beim Export als einzelne Datei - und stellt fest, dass das für viele Fälle völlig ausreicht.

Quellen (geprüft am 15.09.2026): FastHTML · Deployment-Beispiele von Answer.AI · HTMX

“Unser Leben ist das, wozu unser Denken es macht.”

- Marc Aurel