st.session_state para persistir datos entre reruns

Intermedio
Streamlit
Streamlit
Actualizado: 26/04/2026

El problema del rerun y por qué necesitamos session_state

El modelo de ejecución de Streamlit es fundamentalmente diferente al de un servidor web tradicional. En Streamlit, cada interacción del usuario provoca la re-ejecución completa del script de arriba a abajo. Esto significa que todas las variables Python declaradas en el ámbito global del script se reinicializan en cada ciclo de ejecución, perdiendo cualquier modificación realizada en el ciclo anterior:

import streamlit as st

# ❌ Esto NO funciona: cada rerun reinicia contador a 0
contador = 0

if st.button("Incrementar"):
    contador += 1  # Se ejecuta, pero contador vuelve a 0 en el próximo rerun

st.write(f"Contador: {contador}")  # Siempre muestra 0 o 1 pero nunca acumula

El ciclo de rerun se dispara en cada interacción y reinicia las variables locales, pero respeta st.session_state y los valores cacheados:

sequenceDiagram
    participant U as Usuario
    participant B as Navegador
    participant S as Streamlit Server
    participant Sc as app.py (script)
    participant SS as session_state
    U->>B: Cambia widget (click, slider)
    B->>S: WebSocket: nuevo valor
    S->>Sc: Re-ejecuta script desde arriba
    Sc->>SS: Lee y actualiza session_state
    Sc-->>S: Árbol de elementos nuevo
    S-->>B: Diff UI
    B-->>U: Render actualizado

st.session_state es un diccionario especial que persiste entre reruns durante la sesión del usuario:

import streamlit as st

# ✅ Esto SÍ funciona con session_state
if "contador" not in st.session_state:
    st.session_state.contador = 0  # Inicializar solo la primera vez

if st.button("Incrementar"):
    st.session_state.contador += 1

st.write(f"Contador: {st.session_state.contador}")  # Acumula correctamente

Cada pestaña del navegador tiene su propia instancia de session_state. Si un usuario abre la misma aplicación en dos pestañas, cada una mantiene un estado completamente independiente, como si fueran sesiones diferentes.

Sintaxis de session_state

st.session_state se comporta como un diccionario Python estándar con soporte adicional para la notación de punto. Todas las operaciones habituales de diccionario (lectura, escritura, verificación de existencia, eliminación) están disponibles:

import streamlit as st

# Inicialización idiomática (múltiples variables)
defaults = {
    "usuario": None,
    "autenticado": False,
    "mensajes": [],
    "modelo_entrenado": False,
    "df_resultado": None
}
for clave, valor in defaults.items():
    if clave not in st.session_state:
        st.session_state[clave] = valor

# Tres formas equivalentes de leer
nombre = st.session_state["usuario"]
nombre = st.session_state.usuario
nombre = st.session_state.get("usuario", "Anónimo")  # Con valor por defecto

# Tres formas equivalentes de escribir
st.session_state["autenticado"] = True
st.session_state.autenticado = True

# Verificar existencia
if "modelo_entrenado" in st.session_state:
    st.success("Modelo disponible.")

# Eliminar una clave
del st.session_state["df_resultado"]

# Listar todas las claves
st.write(list(st.session_state.keys()))

Vincular widgets a session_state con key

Cada widget de Streamlit puede vincularse a una clave de session_state mediante el parámetro key. Cuando un widget tiene un key, su valor actual se sincroniza automáticamente con st.session_state[key], creando una correspondencia bidireccional entre la interfaz visual y el estado interno:

import streamlit as st

# La clave "nombre_usuario" en session_state siempre refleja el valor actual del input
nombre = st.text_input("Tu nombre", key="nombre_usuario")

# Acceso equivalente:
# st.session_state["nombre_usuario"]
# nombre (variable devuelta directamente)

# Acceder desde session_state desde otro lugar del código
if st.session_state.get("nombre_usuario"):
    st.write(f"Hola, {st.session_state.nombre_usuario}!")

Regla: No asignes el valor inicial de un widget con key antes de que aparezca el widget. Inicializa en session_state y el widget leerá ese valor.

Implementar un wizard de pasos

import streamlit as st

if "paso" not in st.session_state:
    st.session_state.paso = 1
    st.session_state.datos_paso1 = {}
    st.session_state.datos_paso2 = {}

st.title("Configuración del análisis")
st.progress(st.session_state.paso / 3, text=f"Paso {st.session_state.paso} de 3")

if st.session_state.paso == 1:
    st.subheader("1. Selección de datos")
    fuente = st.selectbox("Fuente de datos", ["CSV", "Base de datos", "API"])
    if st.button("Siguiente →", type="primary"):
        st.session_state.datos_paso1["fuente"] = fuente
        st.session_state.paso = 2
        st.rerun()

elif st.session_state.paso == 2:
    st.subheader("2. Configuración del modelo")
    modelo = st.selectbox("Algoritmo", ["Random Forest", "XGBoost", "SVM"])
    umbral = st.slider("Umbral de decisión", 0.0, 1.0, 0.5)
    col1, col2 = st.columns(2)
    with col1:
        if st.button("← Anterior"):
            st.session_state.paso = 1
            st.rerun()
    with col2:
        if st.button("Siguiente →", type="primary"):
            st.session_state.datos_paso2 = {"modelo": modelo, "umbral": umbral}
            st.session_state.paso = 3
            st.rerun()

elif st.session_state.paso == 3:
    st.subheader("3. Confirmación")
    st.write(f"**Fuente:** {st.session_state.datos_paso1['fuente']}")
    st.write(f"**Modelo:** {st.session_state.datos_paso2['modelo']}")
    st.write(f"**Umbral:** {st.session_state.datos_paso2['umbral']}")
    col1, col2 = st.columns(2)
    with col1:
        if st.button("← Anterior"):
            st.session_state.paso = 2
            st.rerun()
    with col2:
        if st.button("✓ Iniciar análisis", type="primary"):
            st.success("¡Análisis iniciado con la configuración seleccionada!")
            st.session_state.paso = 1  # Reiniciar wizard

Acumular mensajes de chat con session_state

import streamlit as st

if "historial" not in st.session_state:
    st.session_state.historial = []

# Mostrar historial
for msg in st.session_state.historial:
    with st.chat_message(msg["rol"]):
        st.write(msg["texto"])

# Input del usuario
if prompt := st.chat_input("Escribe un mensaje..."):
    st.session_state.historial.append({"rol": "user", "texto": prompt})
    respuesta = f"Echo: {prompt}"  # Aquí iría la llamada real al LLM
    st.session_state.historial.append({"rol": "assistant", "texto": respuesta})
    st.rerun()

if st.session_state.historial:
    if st.button("Limpiar conversación"):
        st.session_state.historial = []
        st.rerun()

Contexto: por qué session_state es el corazón de cualquier app Streamlit no trivial

En una aplicación Streamlit real, rara vez basta con mostrar un dataframe y un par de gráficos. En cuanto quieres implementar un formulario multipaso, acumular resultados de análisis sucesivos, autenticar usuarios, mantener un carrito de compra o construir un chatbot, necesitas almacenar información que sobreviva entre reruns. Sin st.session_state, cada interacción borraría todo el contexto y la aplicación sería incapaz de recordar incluso al usuario que acaba de iniciar sesión.

Es importante entender que session_state no es una base de datos compartida. Cada pestaña del navegador tiene su propia sesión independiente. Si quieres compartir datos entre usuarios o conservarlos entre sesiones, necesitas integrar una base de datos externa, Redis o ficheros en disco. session_state es, esencialmente, una memoria RAM temporal ligada al ciclo de vida de la conexión WebSocket con el servidor.

Patrones idiomáticos de inicialización

Existen tres patrones recomendados para inicializar claves en session_state, y elegir el correcto depende del contexto:

  1. Guard clause individual: if "clave" not in st.session_state: st.session_state.clave = valor. Ideal cuando tienes una o dos claves.
  2. Diccionario de defaults: iterar sobre un dict de valores por defecto al inicio del script. Es la forma más limpia cuando el estado inicial tiene muchas claves.
  3. Lazy initialization con .setdefault: st.session_state.setdefault("clave", valor). Útil cuando la clave solo se necesita en una rama concreta del código.

Explicación línea por línea del wizard de 3 pasos

El ejemplo del asistente de configuración en tres pasos es un patrón extremadamente común en apps Streamlit. Desglosado:

  1. El primer if inicializa paso = 1 y los dos diccionarios que recogerán los datos de cada pantalla.
  2. st.progress(st.session_state.paso / 3, ...) dibuja una barra de progreso que refleja el avance.
  3. Cada bloque if/elif renderiza la UI de un paso distinto; solo uno está visible a la vez.
  4. Al pulsar "Siguiente", se guardan los datos del paso actual en el diccionario correspondiente y se incrementa el contador.
  5. st.rerun() fuerza una re-ejecución inmediata para que la nueva pantalla aparezca sin demora (si no se llama, el usuario tendría que interactuar otra vez para ver el cambio).
  6. Los botones "Anterior" funcionan decrementando el paso sin tocar los datos, lo que permite al usuario corregir sin perder el progreso.

Errores comunes con session_state

Modificar el valor de un widget con key antes de crearlo. Si haces st.session_state.mi_input = "nuevo" y luego renderizas st.text_input("X", key="mi_input"), Streamlit lanzará un warning. La forma correcta es inicializar el valor con la primera creación del widget o con un bloque if "mi_input" not in st.session_state: previo al widget.

Olvidar que los reruns son el estado por defecto. Muchos recién llegados añaden st.rerun() en cualquier lugar "por si acaso". Es innecesario: Streamlit ya provoca un rerun tras cualquier interacción. Usa st.rerun() solo cuando necesites forzar un rerun adicional inmediatamente después de modificar session_state.

Tratar session_state como una base de datos global. Cada usuario y pestaña tiene su propio session_state. No puedes usarlo para compartir información entre sesiones diferentes.

Estado incoherente entre widgets. Si modificas session_state.paso dentro de un callback pero también lo cambias manualmente más abajo, pueden producirse conflictos. Centraliza la lógica de cambio de estado en un único lugar por cada clave.

Pérdida de estado al recargar la página. Al presionar F5 el usuario empieza una nueva sesión y session_state se vacía. Si necesitas persistencia entre recargas, guarda en disco, base de datos o cookies.

Mejores prácticas

  • Inicializa todas las claves al principio del script, antes del primer widget, para evitar KeyError inesperados.
  • Usa nombres de clave descriptivos y con prefijos cuando la app tiene muchas secciones (login_usuario, analisis_df, config_modelo).
  • Evita almacenar objetos muy grandes (DataFrames de millones de filas, modelos completos) directamente en session_state. Usa @st.cache_resource para esos casos.
  • Para depurar el estado, añade temporalmente st.write(st.session_state) al final del script. Verás el diccionario completo en la página.
  • Envuelve lógica compleja de transición de estado en funciones auxiliares que reciban st.session_state como argumento implícito, para mantener el script principal legible.
Alan Sastre - Autor del tutorial

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

Entender por qué las variables Python se resetean en cada rerun y cómo evitarlo. Inicializar y leer variables en st.session_state de forma idiomática. Modificar session_state desde callbacks on_change y on_click. Implementar flujos de pasos (wizard) y estado acumulado con session_state. Vincular widgets a session_state usando el parámetro key.