Persisting UI State Across Streamlit Reruns with st.session_state: A Counter Walkthrough
Streamlit reruns your whole script on every interaction, wiping plain variables. Learn the check-initialize-update pattern for st.session_state with a working counter app, verification steps, and its real limits.
04 Oct 2025, 08:09 UTC

Why your counter resets to zero
Streamlit reruns your entire script top to bottom every time a user clicks a button, moves a slider, or changes any widget. A plain Python variable like count = 0 is recreated on every rerun, so any value the user built up disappears the moment they interact with the page. The fix is st.session_state: a dictionary-like object that Streamlit keeps alive for the duration of a browser session, so values you store in it survive every rerun.
This guide builds a small counter app that initializes, increments, displays, and resets a value using st.session_state, then shows how to verify the behavior and where the limits are.
Prerequisites
- Python 3.9 or later with Streamlit installed (
pip install streamlit). The examples assume Streamlit 1.x, wherest.session_stateis stable API. - Ability to run commands in a terminal with write access to a working directory.
- No accounts, servers, or external services are needed; Streamlit runs locally.
The pattern: check, initialize, update
Every st.session_state workflow follows the same three-step pattern:
- Check whether the key exists in
st.session_state. - Initialize it with a default if it does not.
- Update it in response to user actions.
The check-and-initialize step must come before any widget reads the key. If you assign a widget-bound key after the widget has already been instantiated in the same run, Streamlit raises an exception, and modifying keys mid-run in ad-hoc ways can produce values that are one rerun behind what you see on screen. Initializing everything up front avoids this class of bug entirely.
Building the counter
Create a file named counter.py in your working directory:
import streamlit as st
st.title("Persistent counter")
# 1. Check and initialize before any interaction
if "count" not in st.session_state:
st.session_state.count = 0
# 2. Update in response to user actions
if st.button("Increment"):
st.session_state.count += 1
if st.button("Reset"):
st.session_state.count = 0
# 3. Display the current value
st.write(f"Current count: {st.session_state.count}")Run it from the terminal in the same directory (no elevated permissions required):
streamlit run counter.pyStreamlit starts a local server (default http://localhost:8501) and opens the app in your browser. The placeholder logic worth understanding:
if "count" not in st.session_stateruns only on the very first rerun of the session. After that, the key exists and the default is never reapplied.st.button("Increment")returnsTrueonly on the rerun triggered by that click, so the increment happens exactly once per click.- The final
st.writereads the key after all updates, so the displayed value always reflects the latest click.
Verifying the behavior
Work through these checks in the browser:
- Click Increment several times. The displayed count should increase by one each time and never jump backward.
- Interact with the page in any other way (resize the window, click elsewhere) — the count stays put, because the value lives in session state rather than a rerun-scoped variable.
- Refresh the page with the browser's reload button. The count should persist, because a refresh reconnects to the same session on the Streamlit server.
- Click Reset. The count returns to 0 and stays there until the next increment.
If the count resets on refresh, the most common cause is that the browser blocked the session cookie or you opened the app in a private window with storage restrictions — check in a normal window first.
Limitations to design around
- Session state is per browser tab. Two tabs, two browsers, or two users each get independent counters. There is no built-in sharing; if you need shared state, use an external store such as a database or Redis.
- State is lost when the server session ends. Restarting the Streamlit process, or a session timing out, clears everything. Session state is not a persistence layer for data that must survive restarts.
- Memory grows with what you store. Each active session holds its own copy of every key. Storing large dataframes or model objects per session multiplies memory usage by concurrent users; keep only essential scalars and identifiers in session state and reload heavy data with
st.cache_datainstead. - Widget-bound keys have rules. When you pass
key="my_input"to a widget, Streamlit manages that key. Set its default before the widget is created, and do not assign to it afterward in the same run.
Recovery options
If state gets into a confusing state during development, two escape hatches exist. First, st.session_state.clear() wipes every key for the current session — useful behind a debug button while iterating. Second, the browser menu (hamburger icon, top right) has a "Rerun" option, and simply restarting the streamlit run process resets all sessions. Neither change is destructive beyond the in-memory state itself, so no rollback procedure is needed for the counter example — the only state involved is the count value.
Once the check-initialize-update pattern is familiar, the same structure covers form inputs, multi-step wizards, and user selections: pick a key, give it a default before widgets render, and update it only inside event-driven blocks.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.