Counters, multi-step flows, chat histories and any app that remembers anything depend on session state.
st.session_state is a dictionary-like object that persists across reruns for one user session (one browser tab). Use attribute or key syntax:
if "count" not in st.session_state:
st.session_state.count = 0
st.session_state.count += 1Always initialize keys before use, because the first run starts empty.
Widgets and state are linked: a widget with key="city" stores its value in st.session_state.city. You can read it anywhere, and you can set it before the widget is created to change the widget programmatically. Setting it after the widget was created in the same run raises an error.
Callbacks run before the rerun's script, which makes state updates take effect immediately:
st.button("+1", on_click=increment, args=(1,))
st.text_input("Name", key="name", on_change=normalize_name)Without a callback, if st.button(...): st.session_state.count += 1 updates the state after any earlier lines have already displayed the old value, a common source of "off by one click" bugs.
Session state is per tab: a page refresh or a new tab starts a new, empty session. For data that must survive restarts or be shared between users, use a database or file, not session state.
Widget values are removed from session state when the widget is not rendered in a run (for example on another page), so copy values you need to keep into your own keys.
Example
import streamlit as st
if "count" not in st.session_state: # initialize once per session
st.session_state.count = 0
st.session_state.history = []
def add(step): # callback: runs before the script
st.session_state.count += step
st.session_state.history.append(step)
def shout():
st.session_state.name = st.session_state.name.upper()
st.button("+1", on_click=add, args=(1,))
st.button("+10", on_click=add, args=(10,))
st.write("Count:", st.session_state.count) # already updated: no lag
st.write("Steps:", st.session_state.history)
st.text_input("Name", key="name", on_change=shout) # value lives in state
st.write("Stored:", st.session_state.get("name", ""))Remember
st.session_statepersists values across reruns within one browser tab- Widget
keys map widget values into session state - Callbacks (
on_click,on_change) update state before the script reruns
Watch out for
- Updating state inside
if st.button(...)after the value was already displayed, so the page lags one click behind.