Skip to content

Streamlit Session State: How to Persist Values Across Reruns

Updated on

st.session_state is Streamlit's built-in way to remember values for one user session across reruns.

If you have ever clicked a button, changed a filter, and watched your app "forget" what just happened, session state is usually the missing piece. It helps Streamlit apps feel continuous instead of stateless.

In Streamlit, the script reruns from top to bottom whenever a widget changes. A "session" is one user's live interaction with the app. Session state is the place where Streamlit keeps values that should survive those reruns for that one session.

Why session state matters

Without session state, many apps feel awkward:

  • counters reset
  • multi-step flows collapse
  • filters lose their previous values
  • forms behave like each rerun is a fresh start

With session state, you can build apps that remember what the user already did.

That usually means better:

  • interactivity
  • flow control
  • user experience
  • predictability in multi-step apps

After reading this guide, you should be able to:

  • keep values alive across reruns
  • bind widgets directly to state
  • build callbacks and multi-step flows cleanly
  • know when session state is the right tool and when caching is better

How session state works

This concept becomes much easier once you picture the rerun cycle.

Diagram showing how Streamlit session state survives reruns

The important idea is simple:

  • a widget interaction triggers a rerun
  • the script starts again from top to bottom
  • st.session_state carries selected values, counters, steps, and drafts into that next rerun

Without session state, the rerun still happens, but the app has much less memory of what the current user just did.

This is also why session state feels abstract at first. It is not a visible widget by itself. It is the per-session memory layer that makes Streamlit's rerun model feel continuous.

Quick Start

import streamlit as st
 
if "count" not in st.session_state:
    st.session_state.count = 0
 
if st.button("Increment"):
    st.session_state.count += 1
 
st.write("Count:", st.session_state.count)

Every button click reruns the app, but the counter stays because the value lives in st.session_state.

What st.session_state is good for

Use session state for values that belong to one user's ongoing interaction, such as:

  • counters
  • filter selections
  • wizard steps
  • selected tabs or views
  • draft form values
  • user-specific context

Do not use it for:

  • global shared models
  • reusable database clients
  • expensive data loading

Those belong in st.cache_resource or st.cache_data.

Initialize state safely

The most common error is reading a key before it exists.

Pattern 1: explicit guard

import streamlit as st
 
if "theme" not in st.session_state:
    st.session_state.theme = "light"

Pattern 2: setdefault

import streamlit as st
 
st.session_state.setdefault("page_size", 25)

Both patterns are fine. The important thing is to initialize before you read.

Widgets and session state

Any widget with a key is automatically added to session state.

import streamlit as st
 
st.text_input("Project name", key="project_name")
st.selectbox("Environment", ["dev", "staging", "prod"], key="env")
 
st.write(st.session_state.project_name)
st.write(st.session_state.env)

This is one of the nicest parts of Streamlit's design. You often do not need a separate state container or a manual sync step. The widget and the state key can be the same thing.

A good mental model is: widgets collect input, and session state remembers the latest input for the current user session.

Use callbacks for cleaner interaction logic

Callbacks run first, then Streamlit reruns the page. They are useful when one interaction should update several values or trigger a state transition.

import streamlit as st
 
st.session_state.setdefault("name", "")
st.session_state.setdefault("submitted_name", "")
 
 
def save_name() -> None:
    st.session_state.submitted_name = st.session_state.name.strip()
 
 
st.text_input("Your name", key="name")
st.button("Save", on_click=save_name, type="primary")
 
if st.session_state.submitted_name:
    st.success(f"Saved: {st.session_state.submitted_name}")

Widgets that support callbacks

  • on_change works with input widgets such as st.text_input, st.selectbox, and st.slider
  • on_click works with button-style widgets such as st.button and st.form_submit_button

Practical patterns

Toggle advanced options

import streamlit as st
 
st.session_state.setdefault("show_advanced", False)
 
if st.button("Toggle advanced options"):
    st.session_state.show_advanced = not st.session_state.show_advanced
 
if st.session_state.show_advanced:
    st.checkbox("Enable audit logging")
    st.checkbox("Use experimental prompt template")

Multi-step flow

import streamlit as st
 
st.session_state.setdefault("step", 1)
 
if st.session_state.step == 1:
    st.write("Step 1: Upload data")
    if st.button("Continue"):
        st.session_state.step = 2
        st.rerun()
 
elif st.session_state.step == 2:
    st.write("Step 2: Configure model")
    if st.button("Back"):
        st.session_state.step = 1
        st.rerun()

Remember filter selections

import streamlit as st
 
st.multiselect(
    "Regions",
    ["APAC", "EMEA", "LATAM", "North America"],
    key="selected_regions",
)
 
st.write("Current filters:", st.session_state.selected_regions)

Session state with forms

Forms batch widget updates until submission. Inside a form, only st.form_submit_button can have a callback.

import streamlit as st
 
st.session_state.setdefault("email", "")
st.session_state.setdefault("saved_email", "")
 
 
def persist_email() -> None:
    st.session_state.saved_email = st.session_state.email
 
 
with st.form("profile_form"):
    st.text_input("Email", key="email")
    st.form_submit_button("Save profile", on_click=persist_email)
 
if st.session_state.saved_email:
    st.write("Saved email:", st.session_state.saved_email)

What session state improves in real projects

Session state is one of the features that lets Streamlit move beyond simple demos.

It can help you build:

  • step-by-step data workflows
  • interactive dashboards with sticky filters
  • annotation or review tools
  • prototype apps that remember what the user already selected
  • form-driven admin tools

That is why it matters. It is not just a syntax trick. It is a capability that lets the app behave more like an actual application.

Common errors and limits

1. Missing-key exceptions

You tried to read a key before initializing it. Add an if key not in st.session_state guard or use setdefault.

2. Modifying a widget's value after it has been created

If a widget already exists on the page, do not assign a new value to the same key later in that run. Initialize first, or update through callbacks and reruns.

3. Treating session state like a button state store

Button-like widgets such as st.button, st.download_button, and st.file_uploader are not designed to have their widget value set manually through session state assignment.

4. Losing state after reload

Session state is tied to the browser session and the WebSocket connection. A browser reload or navigation event can reset it.

5. Storing too much data

You can store DataFrames, lists, and custom objects, but remember that session state lives in memory.

Session state vs caching

These tools solve different problems.

NeedUse
Keep a user's current filtersst.session_state
Remember which step a user is onst.session_state
Avoid rerunning a slow queryst.cache_data
Reuse a model across usersst.cache_resource

If the value is user-specific, use session state. If the goal is performance, use caching.

Advanced note: serializable session state

Streamlit can optionally enforce that session state values are serializable through the runner.enforceSerializableSessionState config setting. Most apps do not need this, but some stricter deployment environments do.

Troubleshooting

Why does my state not update immediately?

Remember the order of execution: callback first, then full rerun. If you update state in a callback, read the updated value after the rerun.

Why does my text input keep resetting?

You are probably:

  • recreating the widget with a different key
  • overwriting the same state key later in the script
  • resetting state during every rerun

Can I persist session state across pages?

Yes. Streamlit supports session state across pages in a multipage app as long as the session stays alive.

Can I store a DataFrame in session state?

Yes, but use it for user-specific working data, not as a global cache. Large shared datasets belong in cached functions.

Related Guides

Frequently Asked Questions

What is st.session_state in simple terms?

It is Streamlit's way of remembering values for one user session across reruns.

How do I initialize st.session_state?

Initialize each key before reading it with either if "key" not in st.session_state or st.session_state.setdefault("key", value).

Does st.session_state survive reruns?

Yes. That is its main purpose.

What is the difference between session state and cache in Streamlit?

Session state stores per-user interaction data. Cache stores reusable computation results or shared resources to improve performance.