Logotipo de Bitdoze

FastHTML para Principiantes: Crea una UI para tu App en Python

Aprende FastHTML desde cero y crea tu primera interfaz para una app en Python en 5 minutos usando solo Python y HTMX. Sin JavaScript ni herramientas de compilación.

Dragos

Actualizado Publicado 49 min de lectura

FastHTML para Principiantes: Crea una UI para tu App en Python

FastHTML es un framework web en Python creado por Jeremy Howard y Answer.AI que te permite construir aplicaciones web completas usando solo Python. Sin JavaScript, sin herramientas de compilación, sin lenguajes de plantillas. Si alguna vez quisiste crear una interfaz para tu app en Python pero no querías aprender React, Vue o incluso HTML/CSS desde cero, este tutorial de FastHTML para principiantes te llevará hasta allí en minutos.

FastHTML funciona mapeando funciones de Python directamente a elementos HTML. Escribes P("Hello") en lugar de <p>Hello</p>. Incluye HTMX 2.x para la interactividad y Pico CSS para el estilo predeterminado, ambos integrados sin configuración adicional. El resultado: pasas de un script de Python a una app web funcional con una UI interactiva más rápido que con cualquier otro framework web en Python.

  • Instalar FastHTML con pip o uv
  • Entender la sintaxis de función a HTML
  • Construir tu primera página web con un servidor funcional
  • Crear componentes de UI comunes (formularios, tablas, barras de navegación)
  • Añadir interactividad con HTMX, sin necesidad de JavaScript
  • Construir una aplicación de tareas completa
  • Solucionar errores comunes
  • Ejecutar tu app de FastHTML en producción en un VPS

Si estás explorando frameworks de UI en Python en general, también echa un vistazo a nuestro tutorial de NiceGUI para principiantes, otra opción para construir interfaces impulsadas por Python.

¿Por qué FastHTML?

El desarrollo web tradicional te obliga a hacer malabares con HTML, CSS, JavaScript y frameworks como React o Vue solo para poner un botón en pantalla. FastHTML elimina todo eso manteniendo todo en Python. Esto es lo que lo hace merecer una oportunidad:

  • Un solo lenguaje: backend y frontend en Python, sin cambio de contexto
  • HTMX primero: interactividad sin escribir JavaScript. HTMX 2.x viene incluido de serie
  • Pico CSS incluido: tus páginas se ven bien desde el primer momento con fast_app(), sin necesidad del CDN de Tailwind
  • Sin herramientas de compilación: sin npm, sin webpack, sin node_modules. Solo son archivos de Python
  • Sintaxis nativa de Python: funciones familiares de Python en lugar de lenguajes de plantillas
  • Construido sobre Starlette/ASGI: base lista para producción, se integra en cualquier host ASGI

Valores predeterminados del framework

fast_app() de FastHTML incluye Pico CSS y recarga en vivo de forma predeterminada. HTMX 2.x (>=2.0.4) viene integrado, sin necesidad de importación separada ni enlace a CDN. Obtienes una página con estilo e interactiva con configuración cero.

FastHTML va por un carril distinto al de Django o Flask. Esos son excelentes frameworks de backend, pero construir una UI con ellos implica escribir plantillas HTML o combinarlos con un frontend en JavaScript. FastHTML te lo da todo en Python. Puedes comparar FastHTML con Django, Flask y FastAPI en nuestra guía de frameworks web de Python, o consultar Streamlit vs. NiceGUI para otros enfoques de UI en Python.

Serie FastHTML

A continuación están los artículos de esta serie para ayudarte a construir con FastHTML:

Instalación de FastHTML

Requisitos previos

  • Python 3.10 o superior (se recomienda 3.11/3.12). FastHTML no funciona con Python 3.9 o inferior.
  • Acceso a la terminal en cualquier SO (macOS, Linux, Windows)

Si necesitas instalar o actualizar Python en macOS, consulta nuestra guía sobre cómo instalar Python en macOS. Para otros sistemas operativos, visita python.org.

pip (predeterminado)

Crea un entorno virtual e instala FastHTML:

bash
python3 -m venv fhenv
source fhenv/bin/activate
# On Windows:
# fhenv\Scripts\activate

Esto crea un entorno aislado para tu proyecto, evitando conflictos de paquetes con el Python de tu sistema.

bash
pip install python-fasthtml

Esto descarga el paquete FastHTML y sus dependencias. También puedes consultar la documentación oficial de FastHTML para más detalles.

uv (más rápido)

uv es un gestor de paquetes de Python rápido escrito en Rust. Si no lo has usado antes, consulta nuestra guía sobre primeros pasos con uv.

bash
uv init fh-app
cd fh-app
uv add python-fasthtml

Para ejecutar tu app:

bash
uv run main.py

uv resuelve e instala dependencias significativamente más rápido que pip y gestiona los entornos virtuales automáticamente.

¿ModuleNotFoundError?

Si ves ModuleNotFoundError: No module named 'fasthtml', casi siempre significa que tu versión de Python es inferior a 3.10 o que olvidaste activar tu entorno virtual. Compruébalo con python3 --version y asegúrate de que which python3 apunte a tu venv.

Entendiendo la sintaxis de FastHTML

La característica destacada de FastHTML es su capacidad de permitirte escribir páginas web usando funciones de Python que se mapean directamente a etiquetas HTML. En lugar de hacer malabares con archivos HTML separados, defines la estructura de tu página en Python:

python
from fasthtml.common import *

# Creating a paragraph element
paragraph = P("Hello, World!")

P es una función de FastHTML que genera <p>Hello, World!</p> al renderizarse. FastHTML proporciona funciones similares para todos los elementos HTML estándar:

HTML FastHTML Ejemplo
<p> P() P("Text")
<h1> H1() H1("Heading")
<div> Div() Div(P("Child element"))
<a> A() A("Link text", href="https://example.com")
<input> Input() Input(type="text", name="username")

Cómo funcionan las funciones de FastHTML:

Cada función acepta:

  • Argumentos posicionales para elementos hijos o contenido
  • Argumentos con nombre para atributos HTML

Por ejemplo, P("Hello", cls="greeting") produce <p class="greeting">Hello</p>.

FastHTML mapea dos palabras clave reservadas de Python para evitar errores de sintaxis:

  • class → cls (ya que class es una palabra clave de Python)
  • for → fr (ya que for es una palabra clave de Python)

El import comodín from fasthtml.common import * es intencional y está explícitamente respaldado por el equipo de FastHTML. Incorpora todos los componentes estándar, helpers y la función serve() que necesitas.

Construyendo tu primera página web

Vamos a crear una página web simple. Crea un archivo llamado main.py:

python
from fasthtml.common import *

app, rt = fast_app()

@rt("/")
def get():
    return Titled("FastHTML Basics",
        P("Below is a list of features:"),
        Ul(
            Li("Easy to learn"),
            Li("Python-based"),
            Li("Dynamic and responsive")
        )
    )

serve()

Vamos a desglosarlo:

  1. from fasthtml.common import * importa todos los componentes y helpers de FastHTML
  2. app, rt = fast_app() crea la aplicación y devuelve un decorador de rutas. fast_app() aplica valores predeterminados útiles: estilos de Pico CSS, recarga en vivo y HTMX 2.x, todo incluido automáticamente
  3. @rt("/") mapea la función a la URL raíz. El nombre de la función get le indica a FastHTML que maneje peticiones GET
  4. Titled() es un helper que genera tanto una etiqueta <title> como un <h1> en una sola llamada
  5. serve() inicia el servidor de FastHTML (comprueba __name__ internamente, así que no necesitas el guard if __name__ == "__main__":)

La forma larga sigue funcionando

A veces verás app = FastHTML() con @app.get("/") en ejemplos antiguos. Eso sigue funcionando, pero fast_app() es el patrón recomendado actualmente. Es lo que usan la documentación oficial y todos los ejemplos recientes.

Verifica que funciona

Ejecuta la app desde tu terminal:

bash
python main.py

Deberías ver una salida como:

text
INFO:     Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)
INFO:     Started reloader process ... using WatchFiles
INFO:     Started server process ...

Abre tu navegador en http://localhost:5001. Deberías ver una página con un encabezado y una lista de viñetas.

También puedes hacer una prueba rápida desde la CLI:

bash
curl -s localhost:5001 | head

Si ves salida HTML, la app está sirviendo correctamente.

Está funcionando

El navegador muestra tu página con los estilos de Pico CSS aplicados automáticamente. Sin CDN de Tailwind, sin archivo CSS, fast_app() se encarga de todo. El recargador en vivo también detecta los cambios de código sin reiniciar manualmente.

Qué ocurre detrás de escena:

  1. FastHTML recibe la petición para /
  2. Llama a la función get()
  3. La función devuelve una página construida con componentes de FastHTML
  4. FastHTML convierte estos componentes a HTML y los envía al navegador

Personalizando los encabezados (CSS/JS)

Pico CSS es el predeterminado con fast_app(). Si prefieres Tailwind o necesitas CSS/JS personalizado, pásalos mediante el parámetro hdrs:

python
app, rt = fast_app(
    pico=False,
    hdrs=(Script(src="https://cdn.tailwindcss.com"),)
)

Establece pico=False para desactivar el Pico CSS predeterminado, luego añade lo que necesites en hdrs.

Componentes básicos de UI

Exploremos los componentes fundamentales para construir interfaces con FastHTML.

Elementos de texto

Los elementos de texto son la base de cualquier interfaz:

python
@rt("/text-elements")
def get():
    return Titled("Text Elements",
        H2("Subheading"),
        H3("Section heading"),
        P("This is a regular paragraph with some ",
          Strong("bold text"), " and some ",
          Em("italicized text"), " mixed in."),
        P("You can also use ", Code("code snippets"), " inline.")
    )

Strong renderiza <strong>, Em renderiza <em>, y Code renderiza <code>. FastHTML sigue la convención de nombres de los elementos HTML. Si conoces HTML, ya conoces FastHTML.

Contenedores y layout

Div es el componente principal para estructurar páginas:

python
@rt("/layout")
def get():
    return Titled("Layout Demo",
        # Sidebar and main content
        Div(
            Div(
                H2("Sidebar"),
                Ul(Li("Home"), Li("About"), Li("Services")),
            ),
            Div(
                H2("Main Content"),
                P("This is the main content area."),
            ),
        )
    )

Con Pico CSS como predeterminado, los contenedores obtienen un espaciado y una tipografía sensatos sin clases extra. Si necesitas layouts personalizados (grids, flexbox), añade tu propio CSS mediante hdrs o usa atributos style en línea.

Enlaces y botones

Los enlaces usan A() y los botones usan Button():

python
@rt("/navigation")
def get():
    return Titled("Navigation Demo",
        Nav(
            A("Home", href="/"),
            A("Features", href="/features"),
            A("Docs", href="/docs"),
            Button("Login"),
        ),
        P("Welcome to FastHTML")
    )

Nav crea un elemento <nav>. Con Pico CSS, las barras de navegación y los botones obtienen un estilo básico automáticamente.

Formularios e inputs

Los formularios permiten a los usuarios enviar datos a tu app. Aquí hay un formulario de contacto:

python
@rt("/contact")
def get():
    return Titled("Contact Us",
        Form(
            Label("Name", Input(type="text", name="name", placeholder="Your name")),
            Label("Email", Input(type="email", name="email", placeholder="Your email")),
            Label("Message", Textarea(name="message", placeholder="Your message", rows=5)),
            Button("Send Message", type="submit"),
            action="/submit-contact",
            method="post"
        )
    )

Observa el patrón de Label — el input está anidado dentro del label, que es el enfoque preferido en la documentación. Si necesitas un atributo for explícito, usa fr (no For):

python
Label("Name:", fr="name")
Input(type="text", id="name", name="name")

FastHTML puede inyectar los valores de los campos del formulario directamente en tu manejador mediante parámetros tipados:

python
@rt("/submit-contact")
def post(name: str, email: str, message: str):
    return P(f"Thanks {name}, we got your message.")

Cuando el formulario se envía, FastHTML extrae name, email y message de los datos del formulario y los pasa como argumentos de la función — sin necesidad de parseo manual.

Manejador ausente = 404

El formulario se envía a /submit-contact. Asegúrate de definir un manejador POST correspondiente (@rt("/submit-contact") def post(...)) o el formulario devolverá un error 404.

Listas y tablas

Organizando datos con listas y tablas:

python
@rt("/data-table")
def get():
    return Titled("User Data",
        Table(
            Thead(
                Tr(Th("ID"), Th("Name"), Th("Email"), Th("Role"))
            ),
            Tbody(
                Tr(Td("1"), Td("John Doe"), Td("[email protected]"), Td("Admin")),
                Tr(Td("2"), Td("Jane Smith"), Td("[email protected]"), Td("User")),
                Tr(Td("3"), Td("Robert Johnson"), Td("[email protected]"), Td("Editor")),
            )
        )
    )

Pico CSS estila automáticamente las tablas con bordes, padding y colores de fila alternos — sin necesidad de clases extra.

Añadiendo interactividad con HTMX

FastHTML integra HTMX — una biblioteca que te permite crear UIs dinámicas sin escribir JavaScript. FastHTML incluye HTMX 2.x (≥2.0.4) de serie, así que no hay nada extra que instalar.

HTMX 2.x incluido

FastHTML incluye HTMX 2.x de fábrica. Sin enlace a CDN ni importación separada. Los atributos hx_* (los guiones bajos se convierten en guiones en el HTML) están todos disponibles como argumentos con nombre.

Aquí hay un ejemplo de contador que se actualiza sin recargar la página completa:

python
from fasthtml.common import *

app, rt = fast_app()

counter = 0

@rt("/")
def get():
    return Titled("HTMX Counter",
        Div(
            P(f"Current count: {counter}", id="counter"),
            Button("Increment",
                  hx_post="/increment",
                  hx_target="#counter"),
            Button("Decrement",
                  hx_post="/decrement",
                  hx_target="#counter"),
        )
    )

@rt("/increment")
def post():
    global counter
    counter += 1
    return P(f"Current count: {counter}", id="counter")

@rt("/decrement")
def post():
    global counter
    counter -= 1
    return P(f"Current count: {counter}", id="counter")

serve()

Ejecútalo, haz clic en Increment y observa cómo el contador se actualiza — sin recarga completa de página. Eso es HTMX en acción.

Referencia de atributos HTMX

FastHTML proporciona los atributos HTMX como argumentos con nombre de Python:

Atributo Qué hace Ejemplo
hx_post Envía una petición POST al hacer clic hx_post="/increment"
hx_get Envía una petición GET al hacer clic hx_get="/items"
hx_target Qué elemento actualizar (selector CSS) hx_target="#counter"
hx_swap Cómo intercambiar la respuesta (innerHTML, outerHTML, beforeend) hx_swap="outerHTML"
hx_trigger Cuándo disparar (click, change, etc.) hx_trigger="change"

Los guiones bajos en los nombres de atributos se convierten en guiones en el HTML renderizado: hx_post → hx-post.

Construyendo una aplicación de tareas

Combinemos todo en una app de tareas funcional con añadir, marcar y eliminar:

python
from fasthtml.common import *
from dataclasses import dataclass

app, rt = fast_app()

todos = []
todo_id_counter = 0

@dataclass
class Todo:
    id: int
    title: str
    completed: bool = False

@rt("/")
def get():
    return Titled("Todo App",
        Form(
            Div(
                Input(type="text", name="title", placeholder="Add a new todo"),
                Button("Add", type="submit"),
            ),
            hx_post="/add-todo",
            hx_target="#todo-list",
            hx_swap="beforeend"
        ),
        Div(id="todo-list"),
    )

@rt("/add-todo")
def post(title: str):
    global todo_id_counter
    if not title.strip():
        return ""
    todo_id_counter += 1
    new_todo = Todo(id=todo_id_counter, title=title)
    todos.append(new_todo)
    return create_todo_item(new_todo)

@rt("/toggle-todo/{id}")
def post(id: int):
    for todo in todos:
        if todo.id == id:
            todo.completed = not todo.completed
            return create_todo_item(todo)
    return ""

@rt("/delete-todo/{id}")
def delete(id: int):
    global todos
    todos = [t for t in todos if t.id != id]
    return ""

def create_todo_item(todo: Todo):
    return Div(
        Div(
            Input(type="checkbox",
                  checked=todo.completed,
                  hx_post=f"/toggle-todo/{todo.id}",
                  hx_target=f"#todo-{todo.id}",
                  hx_swap="outerHTML"),
            Span(todo.title,
                 style="text-decoration: line-through; opacity: 0.6" if todo.completed else ""),
        ),
        Button("×",
               hx_delete=f"/delete-todo/{todo.id}",
               hx_target=f"#todo-{todo.id}",
               hx_swap="outerHTML"),
        id=f"todo-{todo.id}",
    )

serve()

Cómo funciona la app de tareas

  1. Estructura de datos: un dataclass de Python define la forma del elemento de tarea
  2. Añadir: el formulario envía un POST a /add-todo — FastHTML extrae title de los datos del formulario automáticamente. HTMX añade el nuevo elemento a la lista
  3. Marcar: hacer clic en el checkbox envía un POST a /toggle-todo/{id} — el servidor invierte completed y devuelve el HTML actualizado. HTMX lo intercambia en su lugar
  4. Eliminar: el botón × envía una petición DELETE a /delete-todo/{id} — el servidor elimina el elemento y devuelve HTML vacío, que HTMX usa para quitar el elemento de la página

De memoria a almacenamiento persistente

La lista todos = [] se reinicia cada vez que el servidor se reinicia. Está bien para aprender, pero no para nada real. FastHTML incluye Fastlite para la persistencia con SQLite:

python
from fastlite import *

db = database("todos.db")
todos = db.create(Todo, pk="id", transform=True)

Desde la versión 0.14.1, fastlite debe importarse explícitamente (ya no se re-exporta desde fasthtml.common). Esto te da un almacén respaldado por SQLite que sobrevive a los reinicios.

Para el tutorial completo, consulta cómo añadir una base de datos SQLite a FastHTML.

Estado solo para desarrollo

El estado global mutable como counter y todos se rompe si ejecutas más de un proceso worker. Usa session o una base de datos para cualquier cosa más allá de un prototipo.

Novedades de FastHTML desde 2025

FastHTML ha ganado funciones significativas desde que este artículo se publicó por primera vez. Esto es lo que vale la pena conocer:

  • MonsterUI — una biblioteca de componentes estilo shadcn construida sobre FastHTML por Answer.AI. El siguiente paso recomendado después de aprender lo básico.
  • Fastlite — persistencia SQLite mediante fastlite (ver añadir una base de datos SQLite a FastHTML)
  • Sessions — añade un parámetro session a cualquier manejador para estado por usuario
  • Beforeware — guardias de autenticación mediante fast_app(before=Beforeware(...))
  • OAuth integrado — soporte para Google, GitHub, Apple, Auth0 (ver Autenticación en FastHTML)
  • Toasts — setup_toasts(app) + add_toast(session, msg, "success") para mensajes flash
  • APIRouter — divide las rutas en múltiples archivos para apps más grandes (ver FastHTML con múltiples páginas)
  • WebSockets y SSE — exts='ws' para soporte de WebSocket, EventStream para eventos enviados por el servidor
  • fill_form() — vincula instancias de dataclass a campos de formulario automáticamente
  • llms.txt / llms-ctx.txt — documentación amigable para LLMs en fastht.ml para asistentes de código con IA
  • FastHTML Gallery — gallery.fastht.ml para ejemplos e inspiración

La ventaja de FastHTML

El enfoque de FastHTML tiene algunas ventajas claras:

  1. UI impulsada por Python: escribe backend y frontend en un solo lenguaje
  2. Sintaxis declarativa: compone funciones en lugar de escribir plantillas HTML
  3. Interactividad integrada: HTMX viene incluido — no se necesita framework de JavaScript
  4. Sin cambio de contexto: permanece en Python desde la capa de datos hasta la UI
  5. Dependencias mínimas: sin Node.js, sin npm, sin paso de compilación
  6. Iteración rápida: UIs funcionales en minutos

FastHTML es adecuado para:

  • Herramientas internas y paneles
  • Prototipos y MVPs
  • Aplicaciones de visualización de datos
  • Interfaces de administración
  • Cualquier app donde la velocidad de desarrollo importa más que las interacciones complejas del lado del cliente

Si estás comparando enfoques de UI en Python, echa un vistazo a Streamlit vs. NiceGUI — otros dos frameworks en este espacio.

Solución de problemas comunes

ModuleNotFoundError: No module named 'fasthtml'

Causa: Casi siempre una de dos cosas — tu versión de Python es inferior a 3.10, u olvidaste activar tu entorno virtual.

Solución:

bash
python3 --version
# Should show 3.10 or higher

which python3
# Should point to your venv, e.g. /path/to/fhenv/bin/python3

Si Python es demasiado viejo, actualízalo. Si which python3 no apunta a tu venv, ejecuta source fhenv/bin/activate de nuevo.

El puerto 5001 ya está en uso

Causa: Otro proceso está usando el puerto 5001.

Solución: Cambia el puerto pasándolo a serve():

python
serve(port=8000)

Desde la versión 0.12.42, serve() pasa los argumentos con nombre directamente a uvicorn.run, así que cualquier opción de uvicorn funciona.

Bucle infinito de recarga en vivo

Causa: Incompatibilidad conocida con uvicorn ≥0.39.

Solución: Actualiza FastHTML — esto se corrigió en la versión 0.12.39:

bash
pip install --upgrade python-fasthtml
El formulario devuelve 404 al enviarse

Causa: La URL de action de tu formulario no coincide con ningún manejador, o el método HTTP es incorrecto.

Solución: Asegúrate de tener un manejador que coincida tanto con la URL como con el método. Un formulario con action="/submit-contact" y method="post" necesita:

python
@rt("/submit-contact")
def post(name: str, email: str, message: str):
    return P(f"Thanks {name}, we got your message.")

Verifica que el nombre de la función coincida con el método HTTP (get, post, delete, etc.).

Ejecutando FastHTML en producción

serve() de FastHTML es un envoltorio de conveniencia para desarrollo alrededor de uvicorn. Para producción, hay algunas cosas que debes saber.

De serve() a un servidor ASGI de producción

Ejecuta con uvicorn directamente para producción:

bash
uvicorn main:app --host 0.0.0.0 --port 8000

O usa gunicorn con workers de uvicorn para configuraciones multi-proceso:

bash
gunicorn main:app --worker-class uvicorn.workers.UvicornWorker --workers 2 --bind 0.0.0.0:8000

Nunca despliegues con debug=True

debug=True expone páginas de error detalladas en el navegador. Bien para desarrollo, un riesgo de seguridad en producción. Quítalo o ponlo en False antes de desplegar.

Despliega detrás de un proxy inverso

Ejecuta tu app de FastHTML detrás de Caddy, Nginx o Traefik para la terminación TLS, servir archivos estáticos y limitación de tasa. Un Caddyfile básico:

text
yourdomain.com {
    reverse_proxy localhost:8000
}

Caddy gestiona los certificados HTTPS automáticamente. La configuración TLS sin configuración es difícil de superar para quienes operan en solitario.

FastHTML es Starlette/ASGI puro — funciona con cualquier host compatible con ASGI. El estado vive en el proceso o en un archivo SQLite, así que el coste de infraestructura en la nube es cero de forma predeterminada. Solo necesitas un VPS con Linux.

Para hosting, yo uso Hetzner VPS desde 4 €/mes para la mayoría de mis proyectos — fiable, barato y con sede en la UE. Hostinger VPS desde 5,99 $/mes es otra opción económica si prefieres un proveedor diferente.

Los precios de los VPS subieron en todos los proveedores en 2026 — si estás replanteándote una máquina alquilada, consulta qué cambió y cuándo gana un mini PC.

Para contenerizar tu app, consulta nuestra guía sobre cómo ejecutar cualquier app en Python con Docker. Si prefieres un flujo de trabajo PaaS, mira desplegar un proyecto en Python con Dokploy para una configuración de despliegue autoalojada.

Conclusión

Has pasado de cero a una app de FastHTML funcional con interactividad, formularios y una lista de tareas — todo en Python puro. Esto es lo que cubrimos:

  • Instalar FastHTML con pip o uv
  • La sintaxis de función a HTML que hace funcionar a FastHTML
  • Construir páginas con fast_app() y @rt()
  • Crear componentes de UI: formularios, tablas, navegación
  • Añadir interactividad con HTMX sin JavaScript
  • Construir una app de tareas completa con añadir, marcar y eliminar
  • Solucionar errores comunes
  • Ejecutar en producción detrás de un proxy inverso

Desde aquí, los siguientes pasos naturales son:

Y si quieres ver cómo se compara FastHTML con otros frameworks, compara los frameworks web de Python para una visión más amplia.