DataLens

session_state: remembering values across reruns

Keep values between reruns with st.session_state, initialise it safely, connect widgets to it with keys, and react to changes with callbacks.

Level: Intermediate · 6 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

The rerun model resets every normal variable on each interaction. That's why the counter from the rerun module always showed 1. For anything the app must remember (a running total, items added to a list, which step of a wizard the user is on), Streamlit provides st.session_state: a dictionary-like store that survives reruns for as long as the user's browser tab is open.

Analogy

Normal variables are a whiteboard wiped clean before every run. st.session_state is a notebook on the side of the desk that stays put. Each user has their own notebook.

The counter, fixed

Here's the counter from module 2 again, the one that always showed 1. This time the count is kept in st.session_state, which survives from one rerun to the next:

import streamlit as st

if "count" not in st.session_state:
    st.session_state.count = 0

if st.button("Add one"):
    st.session_state.count += 1

st.write("Count:", st.session_state.count)

Three things happen:

  1. Initialise once: if "count" not in st.session_state is only true on the very first run, so count starts at 0 exactly once.
  2. Update: the click adds one to the stored value.
  3. Read: the display uses the stored value, which now survives reruns.

st.session_state.count and st.session_state["count"] are the same thing; use whichever reads better. The in check and .get("count", 0) work as with a dictionary.

Key idea

Always initialise session state with an if key not in st.session_state guard. Without it, the initialisation line runs on every rerun and wipes the stored value, which is the original bug in a new disguise.

Remembering a list

A small "shopping list" of products to review:

import streamlit as st

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

product = st.selectbox("Product", ["USB-C Hub", "Desk Lamp LED", "Pour-over Kettle", "Ceramic Mug"])
if st.button("Add to watchlist") and product not in st.session_state.watchlist:
    st.session_state.watchlist.append(product)
if st.button("Clear"):
    st.session_state.watchlist = []

st.write("Watchlist:", ", ".join(st.session_state.watchlist) or "empty")

The list lives in session state, so it grows across clicks instead of starting empty each time.

Widgets with keys are already in session state

Give a widget a key=, and its value is stored under that key automatically:

import streamlit as st

st.slider("Discount (%)", 0, 50, 10, key="discount")
st.write("Discount from session state:", st.session_state.discount)

This is useful when another part of the app (a callback, another page) needs a widget's value. You can also set a widget's value through session state, but only before the widget is drawn in that run: setting it afterwards raises an error, because the widget already decided its value for this run.

Callbacks: run code when something changes

Widgets accept on_change (or on_click for buttons): a function that runs before the next rerun of the script. Callbacks are the clean way to update state in response to an action:

import streamlit as st

if "count" not in st.session_state:
    st.session_state.count = 0


def add(amount):
    st.session_state.count += amount


def reset():
    st.session_state.count = 0


st.button("+1", on_click=add, args=(1,))
st.button("+10", on_click=add, args=(10,))
st.button("Reset", on_click=reset)
st.metric("Count", st.session_state.count)

Why bother? Because the callback runs before the script, the metric already shows the new value on the rerun. With the if st.button(...) style, any code above the button would still see the old value in that run. With several buttons that change the same state, callbacks keep the logic in small named functions instead of a chain of ifs.

args=(1,) passes arguments to the callback (note the comma: it's a tuple).

What session state is (and isn't)

Why this matters for clean code

Session state is global, mutable state, the kind clean code tries to keep small and explicit. Keep it tidy: initialise every key in one place at the top of the script (or in a function init_state()), name keys clearly, and change state through small callback functions. Then "where does this value change?" has one answer.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

Next: Caching with st.cache_data and st.cache_resource →