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.
Actualizado Publicado 49 min de lectura

FastHTML Tutorial Series
Part 1 of 6
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:
- FastHTML: primeros pasos, este artículo
- FastHTML con múltiples páginas
- Herramientas de IA complejas con FastHTML
- Construir una app web simple con IA usando FastHTML y Pydantic AI
- Añadir historial con base de datos SQLite a tu generador de títulos con IA en FastHTML
- Autenticación en 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:
python3 -m venv fhenv
source fhenv/bin/activate
# On Windows:
# fhenv\Scripts\activateEsto crea un entorno aislado para tu proyecto, evitando conflictos de paquetes con el Python de tu sistema.
pip install python-fasthtmlEsto 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.
uv init fh-app
cd fh-app
uv add python-fasthtmlPara ejecutar tu app:
uv run main.pyuv 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:
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 queclasses una palabra clave de Python)for→fr(ya quefores 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:
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:
from fasthtml.common import *importa todos los componentes y helpers de FastHTMLapp, 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@rt("/")mapea la función a la URL raíz. El nombre de la funcióngetle indica a FastHTML que maneje peticiones GETTitled()es un helper que genera tanto una etiqueta<title>como un<h1>en una sola llamadaserve()inicia el servidor de FastHTML (comprueba__name__internamente, así que no necesitas el guardif __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:
python main.pyDeberías ver una salida como:
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:
curl -s localhost:5001 | headSi 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:
- FastHTML recibe la petición para
/ - Llama a la función
get() - La función devuelve una página construida con componentes de FastHTML
- 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:
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:
@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:
@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():
@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:
@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):
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:
@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:
@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:
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:
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
- Estructura de datos: un dataclass de Python define la forma del elemento de tarea
- Añadir: el formulario envía un POST a
/add-todo— FastHTML extraetitlede los datos del formulario automáticamente. HTMX añade el nuevo elemento a la lista - Marcar: hacer clic en el checkbox envía un POST a
/toggle-todo/{id}— el servidor inviertecompletedy devuelve el HTML actualizado. HTMX lo intercambia en su lugar - 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:
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
sessiona 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,EventStreampara 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:
- UI impulsada por Python: escribe backend y frontend en un solo lenguaje
- Sintaxis declarativa: compone funciones en lugar de escribir plantillas HTML
- Interactividad integrada: HTMX viene incluido — no se necesita framework de JavaScript
- Sin cambio de contexto: permanece en Python desde la capa de datos hasta la UI
- Dependencias mínimas: sin Node.js, sin npm, sin paso de compilación
- 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:
python3 --version
# Should show 3.10 or higher
which python3
# Should point to your venv, e.g. /path/to/fhenv/bin/python3Si 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():
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:
pip install --upgrade python-fasthtmlEl 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:
@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:
uvicorn main:app --host 0.0.0.0 --port 8000O usa gunicorn con workers de uvicorn para configuraciones multi-proceso:
gunicorn main:app --worker-class uvicorn.workers.UvicornWorker --workers 2 --bind 0.0.0.0:8000Nunca 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:
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:
- Construye un sitio web multipágina con FastHTML — enrutamiento, layouts y navegación entre páginas
- Añade una base de datos SQLite a FastHTML — almacenamiento persistente con Fastlite
- Añade autenticación — OAuth, sesiones y control de acceso
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.


