Streamlit re-runs your entire Python script from top to bottom every single time a user interacts with anything — a button click, a slider move, a text input. That is how it works. Most of the time it is fine.

But this creates a real problem: every variable you create in your script gets reset to its starting value on every re-run. So if a user types something in a text box, picks a value from a dropdown, or gets to step 2 of a form — the next interaction wipes it all out.

This is where st.session_state comes in. It is a dictionary that survives every re-run for the whole time the user has the app open. This guide covers everything you need to know to use it well.

The Problem Session State Solves

Before we look at the solution, it helps to see the problem clearly. Here is an app that tries to count button clicks — but it does not work:

Python — this does NOT work, count resets every time
import streamlit as st # This resets to 0 on EVERY re-run — it never increments count = 0 if st.button('Click me'): count += 1 st.write(f'You clicked {count} times') # Always shows "You clicked 0 times" or "You clicked 1 times" # It never goes above 1 because count resets to 0 every re-run

Every time the button is clicked, Streamlit re-runs the script. The first line sets count = 0 again. So the count is always either 0 or 1. It never accumulates. Session state is the fix.

⚠️ Remember: any regular Python variable you create in a Streamlit script lives only for one run. The moment the script finishes and re-runs, that variable is gone. Only st.session_state survives between runs.

What Is Session State

Think of st.session_state like a small notebook that Streamlit keeps open on the desk next to your script. Every time your script re-runs, it can read from and write to that notebook. The notebook stays there the whole time the user has the app open in their browser.

Each entry in the notebook has a name (the key) and a value. You can store anything in it — numbers, strings, lists, DataFrames, even model results.

The notebook is per user, per tab. If two people open your app at the same time, each of them gets their own notebook. If one user opens the app in two browser tabs, each tab gets its own notebook too.

The Basics

Reading and Writing Values

You access session state like a dictionary, or like an object with dot notation. Both work the same way:

Python — reading and writing session state
import streamlit as st # Write a value — two ways to do it, both work st.session_state['username'] = 'shashank' # dictionary style st.session_state.username = 'shashank' # dot notation style # Read a value name = st.session_state['username'] name = st.session_state.username # Check if a key exists before reading it if 'username' in st.session_state: st.write(st.session_state.username) # Delete a value when you no longer need it del st.session_state['username'] # See everything currently stored st.write(dict(st.session_state)) # great for debugging

Setting Default Values Safely

The most important pattern in session state is always check before writing. If you write a value unconditionally, it will reset every time the script re-runs — which defeats the whole purpose.

Python — the right way to set default values
import streamlit as st # WRONG — this resets to 0 on every re-run st.session_state.count = 0 # RIGHT — only sets the default if it does not exist yet if 'count' not in st.session_state: st.session_state.count = 0 # ALSO RIGHT — .setdefault() does the same thing in one line st.session_state.setdefault('count', 0) # Initialise several values at once, cleanly defaults = { 'step': 1, 'username': '', 'logged_in': False, 'cart': [], } for key, value in defaults.items(): if key not in st.session_state: st.session_state[key] = value
The golden rule: always initialise session state values at the top of your script using the if key not in st.session_state pattern. This way they are set once on first run and kept as-is on every subsequent run.

Session State with Widgets

Using Widget Keys

Every Streamlit widget — text inputs, sliders, checkboxes, selectboxes — can be given a key parameter. When you do this, Streamlit automatically links that widget's value to session state. You do not have to store it manually.

Python — widget keys link directly to session state
import streamlit as st # Give every widget a key — Streamlit auto-stores the value st.text_input('Your name', key='name') st.slider('Your age', 0, 100, key='age') st.selectbox('Favourite language', ['Python', 'JavaScript', 'Rust'], key='language') st.checkbox('I agree to the terms', key='agreed') # Now read them from session state anywhere in the script if st.session_state.name: st.write(f'Hello {st.session_state.name}!') st.write(f'Age: {st.session_state.age}') st.write(f'Favourite: {st.session_state.language}') # Only show the submit button when the user has agreed if st.session_state.agreed: st.button('Submit')

Callbacks — Run Code When a Widget Changes

Sometimes you want to run some code immediately when a widget value changes, before the rest of the script runs. You do this with the on_change or on_click parameter and a callback function.

Python — using callbacks with session state
import streamlit as st # Initialise state if 'total' not in st.session_state: st.session_state.total = 0 # Callback runs BEFORE the rest of the script on each change def update_total(): qty = st.session_state.quantity price = st.session_state.price st.session_state.total = qty * price st.number_input('Quantity', min_value=1, value=1, key='quantity', on_change=update_total) st.number_input('Price ($)', min_value=0.0, value=10.0, key='price', on_change=update_total) st.metric('Total', f'${st.session_state.total:.2f}')

Counter Example — The Classic Use Case

Here is the counter we tried to build at the start, now done correctly using session state:

Python — a working click counter using session state
import streamlit as st # Step 1: initialise the count if it does not exist if 'count' not in st.session_state: st.session_state.count = 0 # Step 2: show the current count st.title(f'You clicked {st.session_state.count} times') # Step 3: update state when buttons are clicked col1, col2, col3 = st.columns(3) with col1: if st.button('Add 1'): st.session_state.count += 1 st.rerun() with col2: if st.button('Add 10'): st.session_state.count += 10 st.rerun() with col3: if st.button('Reset'): st.session_state.count = 0 st.rerun()

Multi-Step Forms — Keep Users on the Right Step

One of the most powerful uses of session state is building multi-step wizards. Without session state, clicking Next on step 1 would send the user back to step 1 on the next re-run. With session state, you track which step they are on and always show the right one.

Python — a 3-step onboarding wizard
import streamlit as st # Initialise step and any collected data if 'step' not in st.session_state: st.session_state.update({'step': 1, 'name': '', 'role': '', 'goal': ''}) # A simple progress indicator st.progress(st.session_state.step / 3) st.caption(f'Step {st.session_state.step} of 3') # STEP 1 — Collect the user's name if st.session_state.step == 1: st.header('Step 1 — What is your name?') name = st.text_input('Full name') if st.button('Next') and name: st.session_state.name = name st.session_state.step = 2 st.rerun() # STEP 2 — Collect the user's role elif st.session_state.step == 2: st.header(f'Step 2 — What do you do, {st.session_state.name}?') role = st.selectbox('I am a...', ['Student', 'Developer', 'Data Scientist', 'Other']) col1, col2 = st.columns(2) if col1.button('Back'): st.session_state.step = 1; st.rerun() if col2.button('Next'): st.session_state.role = role st.session_state.step = 3; st.rerun() # STEP 3 — Confirm and submit elif st.session_state.step == 3: st.header('Step 3 — All done!') st.success(f'Welcome {st.session_state.name}, the {st.session_state.role}!') if st.button('Start Over'): st.session_state.step = 1; st.rerun()

Storing Computed Results

One of the best uses of session state is to cache the result of an expensive computation — like running an ML model or querying a database — so you only do it once even if the user changes other things on the page.

Python — store expensive results to avoid recomputing
import streamlit as st import time # Only run the heavy model if we do not already have a result stored if 'prediction' not in st.session_state: st.session_state.prediction = None user_text = st.text_area('Enter some text to analyse') if st.button('Analyse') and user_text: with st.spinner('Running model...'): time.sleep(2) # pretend this is a slow model st.session_state.prediction = { 'label': 'Positive', 'confidence': 0.94, 'text': user_text } # Show the result if we have one — persists through other interactions if st.session_state.prediction: result = st.session_state.prediction st.success(f'Result: {result["label"]} ({result["confidence"]:.0%} confident)') # This slider does NOT re-run the model — result stays visible st.slider('Display font size', 10, 24, 14)
ℹ️ Session state vs st.cache_data: use st.cache_data for results that are the same for everyone, regardless of user. Use st.session_state for results that are specific to one user's inputs in one session. They work well together.

Clearing and Resetting State

Sometimes you want to clear specific values or reset the whole app back to its starting state — for example when the user logs out or clicks a Start Over button.

Python — clearing and resetting session state
import streamlit as st # Delete one specific key if 'token' in st.session_state: del st.session_state['token'] # Reset specific keys to their defaults def reset_form(): st.session_state.step = 1 st.session_state.name = '' st.session_state.role = '' st.session_state.prediction = None # Clear everything in session state — full reset def full_reset(): st.session_state.clear() # Logout example if st.button('Log Out'): full_reset() st.rerun()

Common Patterns at a Glance

Here is a quick reference of the session state patterns you will use most often in real Streamlit apps:

PatternWhen to UseKey Code
Persist a counter Any value that increments across clicks st.session_state.count += 1
Track form step Multi-step wizards and onboarding flows st.session_state.step = 2
Widget key binding Auto-save widget value without manual code st.text_input('Name', key='name')
Store model results Avoid re-running slow code on every interaction st.session_state.result = model.predict(...)
Toggle a flag Show and hide parts of the UI conditionally st.session_state.show_chart = True
Shopping cart / list Accumulate items across multiple interactions st.session_state.cart.append(item)
Debug tip: add st.write(dict(st.session_state)) anywhere in your app while developing. It prints everything in state as a JSON-like dictionary so you can see exactly what is stored and what values it has at any point.

⚡ Key Takeaways
  • Streamlit re-runs your whole script on every interaction. Regular Python variables reset every time. Only st.session_state survives between re-runs.
  • Always initialise state values using if key not in st.session_state — never unconditionally — or they will reset on every run.
  • You can read and write session state using either st.session_state['key'] (dictionary style) or st.session_state.key (dot notation). Both work identically.
  • Give widgets a key parameter to automatically link their value to session state. You do not need to copy the value manually.
  • Use the on_change parameter with a callback function to run code immediately when a widget value changes.
  • Session state is the right tool for multi-step forms — store the current step number and render the right form based on it.
  • Store the result of slow operations (ML models, database queries) in session state so they only run when triggered by the user, not on every slider move.
  • Use st.session_state.clear() to fully reset, or del st.session_state['key'] to remove a single value.