Por qué usar st.form
Por defecto, cada interacción con un widget provoca un rerun completo del script. En aplicaciones con múltiples campos de configuración (parámetros de modelos, filtros complejos, datos de registro), esto genera un problema: cada vez que el usuario ajusta un solo campo, se ejecutan todas las operaciones del script, incluyendo las más costosas.
st.form soluciona este problema agrupando varios widgets en un contenedor que solo provoca un rerun cuando el usuario pulsa el botón de envío. Los cambios en los widgets internos se acumulan sin provocar re-ejecuciones intermedias:
import streamlit as st
# Sin formulario: cada cambio en los widgets ejecuta el análisis
modelo = st.selectbox("Modelo", ["RF", "XGB"]) # rerun al cambiar
n = st.slider("n_estimators", 10, 500) # rerun al mover
ejecutar_analisis(modelo, n) # ¡Se ejecuta demasiado!
# Con formulario: solo se ejecuta al pulsar el botón
with st.form("config_modelo"):
modelo = st.selectbox("Modelo", ["RF", "XGB"])
n = st.slider("n_estimators", 10, 500)
enviado = st.form_submit_button("Entrenar modelo")
if enviado:
ejecutar_analisis(modelo, n) # Solo se ejecuta cuando el usuario pulsa
El primer parámetro de st.form es una cadena identificadora única que Streamlit utiliza para asociar los widgets internos con el formulario correcto. Si se tienen múltiples formularios en la misma aplicación, cada uno debe tener un identificador distinto.
flowchart TD
A[Widgets sueltos] --> B{"Cada cambio rerun completo?"}
B -->|Si| C[Sin form costoso]
D[Bloque st.form id] --> E[Widgets internos sin rerun]
E --> F[Usuario edita varios campos]
F --> G[st.form_submit_button]
G --> H[Click envia todos los valores juntos]
H --> I[Un solo rerun procesando datos]
I --> J{"Validación ok?"}
J -->|Si| K[Procesar y persistir]
J -->|No| L[st.error muestra mensaje]
K --> M[Notificar success]
L --> F
Formulario de registro completo con validación
El siguiente ejemplo muestra un formulario de registro con múltiples campos organizados en columnas, validación de datos al enviar y mensajes de error específicos para cada campo problemático:
import streamlit as st
import re
st.title("Formulario de registro de usuario")
with st.form("registro", clear_on_submit=False):
st.subheader("Datos personales")
col1, col2 = st.columns(2)
with col1:
nombre = st.text_input("Nombre *")
email = st.text_input("Email *")
with col2:
apellido = st.text_input("Apellido *")
telefono = st.text_input("Teléfono")
st.subheader("Configuración de cuenta")
password = st.text_input("Contraseña *", type="password")
password_rep = st.text_input("Repetir contraseña *", type="password")
rol = st.selectbox("Rol", ["Analista", "Desarrollador", "Manager", "Admin"])
notificaciones = st.checkbox("Recibir notificaciones por email", value=True)
enviado = st.form_submit_button("Crear cuenta", type="primary", use_container_width=True)
# Validación y procesamiento fuera del bloque with
if enviado:
errores = []
if not nombre.strip() or not apellido.strip():
errores.append("El nombre y apellido son obligatorios.")
if not email.strip() or not re.match(r'^[\w.+-]+@[\w-]+\.[\w.-]+$', email):
errores.append("Introduce un email válido.")
if len(password) < 8:
errores.append("La contraseña debe tener al menos 8 caracteres.")
if password != password_rep:
errores.append("Las contraseñas no coinciden.")
if errores:
for error in errores:
st.error(error)
else:
st.success(f"Cuenta creada para **{nombre} {apellido}** ({email}) como {rol}.")
if notificaciones:
st.info("Se enviarán notificaciones al email registrado.")
st.form con clear_on_submit
import streamlit as st
with st.form("comentario", clear_on_submit=True): # Limpia los campos al enviar
st.subheader("Añadir comentario")
texto = st.text_area("Comentario *", placeholder="Escribe tu comentario aquí...")
rating = st.slider("Puntuación", 1, 5, 3)
enviado = st.form_submit_button("Publicar comentario")
if enviado:
if texto.strip():
if "comentarios" not in st.session_state:
st.session_state.comentarios = []
st.session_state.comentarios.append({"texto": texto, "rating": rating})
st.success("Comentario publicado.")
else:
st.error("El comentario no puede estar vacío.")
# Mostrar comentarios acumulados
for c in st.session_state.get("comentarios", []):
with st.container(border=True):
st.write(f"{'⭐' * c['rating']} ({c['rating']}/5)")
st.write(c["texto"])
Formularios dentro de expanders y diálogos
import streamlit as st
@st.dialog("Configurar análisis", width="large")
def configurar():
with st.form("config_analisis"):
col1, col2 = st.columns(2)
with col1:
modelo = st.selectbox("Algoritmo", ["RF", "XGB", "SVM"])
n_fold = st.slider("K-Fold CV", 2, 10, 5)
with col2:
test_size = st.slider("Test size (%)", 10, 40, 20)
random_state = st.number_input("Random state", value=42)
metricas = st.multiselect(
"Métricas a evaluar",
["Accuracy", "Precision", "Recall", "F1", "AUC-ROC"],
default=["Accuracy", "AUC-ROC"]
)
guardar = st.form_submit_button("Guardar configuración", type="primary", use_container_width=True)
if guardar:
st.session_state.config_analisis = {
"modelo": modelo, "n_fold": n_fold,
"test_size": test_size / 100, "random_state": random_state,
"metricas": metricas
}
st.rerun()
# Botón en la interfaz principal
if st.button("⚙️ Configurar análisis"):
configurar()
if "config_analisis" in st.session_state:
cfg = st.session_state.config_analisis
st.success(f"Configurado: {cfg['modelo']}, {cfg['n_fold']}-Fold CV, test={cfg['test_size']:.0%}")
Limitaciones importantes de st.form
- No se pueden usar
on_changenion_clicken widgets dentro de unst.form. - No se puede anidar un
st.formdentro de otro. - st.spinner y actualizaciones dinámicas no funcionan dentro de un formulario.
- Los valores de los widgets no están disponibles en session_state hasta después de enviar el formulario.
Para casos donde necesitas interactividad inmediata (por ejemplo, validación en tiempo real), usa widgets normales con session_state en lugar de formularios.
Contexto teórico: por qué los formularios son especiales en Streamlit
Streamlit funciona con un modelo de ejecución top-down, lo que significa que cada modificación de un widget dispara un rerun completo del script. Ese comportamiento es una virtud para la simplicidad del código pero se convierte en un problema en pantallas de configuración con muchos campos: cada ajuste fuerza la re-ejecución de todo el pipeline y si hay operaciones costosas (consulta SQL, entrenamiento de modelo, llamada a una API) la experiencia se degrada.
Los formularios son la solución idiomática de Streamlit para este problema. Internamente, un st.form crea un contenedor que intercepta las señales de cambio de los widgets que viven dentro de él y las mantiene en un búfer local al navegador. Solo cuando el usuario pulsa st.form_submit_button, Streamlit envía todos los valores de una vez al backend y provoca un único rerun con el estado consolidado. Esto reduce drásticamente el tráfico de reruns y es especialmente útil en entornos donde cada rerun tiene coste real (llamadas a LLM, consultas de datos remotos o renderizados pesados).
Explicación línea por línea del ejemplo de registro
with st.form("registro", clear_on_submit=False):abre un contenedor de formulario con identificador"registro". El parámetroclear_on_submit=Falsepreserva los valores tras el envío, útil cuando quieres mostrar los datos introducidos.- Las columnas dentro del formulario permiten un layout a dos bandas sin sacrificar la agrupación semántica.
st.form_submit_button("Crear cuenta", type="primary", use_container_width=True)es obligatorio: todo formulario debe tener al menos un botón de envío para funcionar.- La validación se hace fuera del bloque
withpara tener acceso a los valores ya consolidados después del clic. - El patrón
errores = []con acumulación yfor error in errores: st.error(error)es idiomático: permite mostrar múltiples errores simultáneamente en lugar de solo el primero.
Tabla de parámetros
| Componente | Parámetro | Descripción |
|-----------|-----------|-------------|
| st.form | key | Identificador único (obligatorio si hay varios formularios) |
| st.form | clear_on_submit | Si True, resetea los widgets al enviar (ideal para comentarios) |
| st.form | enter_to_submit | Controla si Enter envía el formulario |
| st.form | border | Dibuja un borde alrededor del formulario |
| st.form_submit_button | label | Texto del botón |
| st.form_submit_button | type | "primary", "secondary" o "tertiary" |
| st.form_submit_button | disabled | Desactiva el envío bajo condiciones |
| st.form_submit_button | use_container_width | Hace que el botón ocupe todo el ancho |
| st.form_submit_button | on_click | Callback opcional que se dispara al enviar |
Errores comunes con formularios
Formulario sin botón de envío. Streamlit lanzará un error indicando que falta el st.form_submit_button. Es el error más frecuente cuando se copia código de widgets sueltos a un formulario.
Uso de on_change dentro del formulario. Streamlit ignorará silenciosamente el callback porque los cambios no se propagan hasta el envío. Si necesitas reaccionar en tiempo real a un cambio, ese widget no debería estar en el formulario.
Leer valores antes de enviar. Los valores de los widgets dentro del formulario no están en st.session_state hasta que se envía. Intentar acceder a st.session_state["mi_campo"] antes del primer envío devolverá KeyError o valor por defecto.
Duplicar keys. Si tienes dos formularios con key="registro", Streamlit fallará. Cada formulario necesita una clave única.
Anidar formularios. No se pueden anidar st.form dentro de otro st.form. Si necesitas varios pasos, usa formularios separados o un asistente con st.session_state que avance por etapas.
Mejores prácticas
- Usa
clear_on_submit=Trueen formularios de entrada recurrente (comentarios, nuevos registros en una tabla). Usaclear_on_submit=Falseen formularios de configuración donde quieres que el usuario vea sus elecciones anteriores. - Agrupa campos relacionados con
st.subheaderdentro del formulario para mejorar la legibilidad. - Válida siempre los datos fuera del
with st.formy muestra errores específicos por campo en lugar de mensajes genéricos. - Combina formularios con
@st.dialogpara configuraciones modales que no saturen la interfaz principal. - Si tu aplicación tiene muchos formularios, guarda los valores enviados en
st.session_statebajo una clave dedicada para facilitar el acceso desde otras secciones del script.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Streamlit es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Streamlit
Explora más contenido relacionado con Streamlit y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Crear formularios con st.form que agrupan widgets para envío conjunto. Usar st.form_submit_button como botón de envío del formulario. Implementar validación de datos del formulario antes de procesarlos. Aprovechar los formularios para evitar reruns innecesarios en configuraciones complejas. Aplicar formularios dentro de columnas y expanders para layouts avanzados.