Session State for Streamlit 🎈

Soon after Streamlit launched in 2019, the community started asking for ways to add statefulness to their apps. Hacks for Session State have been around since October 2019, but we wanted to build an elegant solution that you could intuitively weave into apps in a few lines of code. Today we're excited to release it!

You can now use Session State to store variables across reruns, create events on input widgets and use callback functions to handle events. This powerful functionality helps create apps which can:

  • Perform data/image annotation
  • Support Pagination
  • Add widgets that depend on other widgets
  • Build simple stateful games like Battleship, Tic Tac Toe, etc.
  • And much more - all of this with the simplicity of writing apps that are Python scripts!

💡 If you want to jump right in, check out our demo to see some of the above apps in action or head to our docs for more detailed info on getting started.

Add State to your App

In Streamlit, interacting with a widget triggers a rerun and variables defined in the code get reinitialized after each rerun. But with Session State, it's possible to have values persist across reruns for those instances when you don't want your variables reinitialized.

For example, here's a simple counter that maintains a count value across multiple presses of an increment button. Each button press triggers a rerun but the count value is preserved and incremented (or decremented) across the rerun:

import streamlit as st
st.title('Counter Example')
# Streamlit runs from top to bottom on every iteraction so
# we check if `count` has already been initialized in st.session_state.
# If no, then initialize count to 0
# If count is already initialized, don't do anything
if 'count' not in st.session_state:
	st.session_state.count = 0
# Create a button which will increment the counter
increment = st.button('Increment')
if increment:
    st.session_state.count += 1
# A button to decrement the counter
decrement = st.button('Decrement')
if decrement:
    st.session_state.count -= 1
st.write('Count = ', st.session_state.count)

💡 To continue building on this example, follow along in our Topic Guide: Add State to your App 🤓

The above shows a basic example of how values can persist over reruns, but let's move on to something a little more complex!

Callback functions and Session State API

As part of this release, we're launching Callbacks in Streamlit. Callbacks can be passed as arguments to widgets like st.button or st.slider using the on_change argument.

💡 Curious what a callback is? Wikipedia phrases it well: "a callback, also known as a "call-after" function, is any executable code that is passed as an argument to other code; that other code is expected to call back (execute) the argument at a given time. " Here's a link if you'd like to read more.

With Session State, events associated with changes to a widget or click events associated with button presses can be handled by callback functions. It's important to remember the following order of execution:

Order of Execution: If a callback function is associated with a widget then a change in the widget triggers the following sequence: First the callback function is executed and then the app executes from top to bottom.

Here's an example:

import streamlit as st
def update_first():
    st.session_state.second = st.session_state.first
def update_second():
    st.session_state.first = st.session_state.second
st.title('🪞 Mirrored Widgets using Session State')
st.text_input(label='Textbox 1', key='first', on_change=update_first)
st.text_input(label='Textbox 2', key='second', on_change=update_second)

In the above, we showcase the use of callbacks and session state.  We also showcase an advanced concept, where session state can be associated with widget state using the key parameter.

To read more on this, check out the Advanced Concepts section in the Session State docs and to check out the API in detail visit the State API documentation.

Wrapping up

That's it for the intro to Session State, but we hope this isn't the end of the conversation! We're excited to see how you'll use these new capabilities, and all the new functionalities state will unlock for the community.

To get started, upgrade to the latest release to use st.session_state and callbacks in your apps:

pip install --upgrade streamlit

If you have any questions about these (or about Streamlit in general) let us know below in the comments or on the forum. And make sure to come by the forum or Twitter to share all the cool things you make! 🎈


This is a companion discussion topic for the original entry at

Very excited for the update! However, after updating, my current apps no longer run properly. I’ve reverted back to version 0.83 and they work fine. It seems to be an issue with ScriptRunner, but I don’t really know what that means or if this is even the place to mention it. Do I need to need to make changes to existing functions even if I do not plan to incorporate session state into the app?

Hey @Squaddaffi !

Thank you for reaching out and letting us know. Can you describe a little more of the error you are seeing? Is an error being displayed, or behavior that looks weird.

There may be some minor changes in the application, but I think overall, you should not need to change anything. Happy to help out here!

No error is being displayed in app, in terminal it shows an error that stops execution. I’ve recreated what I’m trying to do in a simple app.

Oooooh. That’s a good find.

Did you say that there’s an error in the terminal output?

If not, if you instrument it (logging or print statements) is it rerunning or just freezing?

It looks like the issue is tied to using a pandas dataframe columns attribute as the options for the multi select. I fixed the issue by changing changing it into a list. Not sure why this broke in 0.84, but it’s working for me now!

Thanks for your patience @Squaddaffi . We have an idea and figured out a workaround in the meantime. I believe you should use list(df.columns) for now here. Essentially, we call the .index standard method on lists on a dataframe index, which doesn’t exist. It should be fixed in the next release. We are investigating how exactly this came into existence to put out the correct fix. We so appreciate your kindness and guidance and sorry for the breakage that was. Let me know if the workaround does not work or if there are any other questions!

1 Like

@Squaddaffi I have opened a new github issue for the problem you reported. It should be an easy fix, once I have determined exactly which part of the system’s incorrect assumptions needs to be changed. Thank you for letting us know about it.

This is really a game changing for Streamlit. I cannot wait to use it. Congratulations


had you try to restart your streamlit app after upgrade?

Hello, thanks for this work around. It seems this does not work for my situation.

Any possible solutions or is waiting the only plausible solution for now?

This is an awesome update, thanks so much! Was waiting for this :partying_face:

Is there a way Streamlit can now expand on this session state management to set secure cookies?
That way we can keep people logged in even after they’ve shut down their browser and set some data when they reopen the page :slightly_smiling_face:

Streamlit is getting more epic with every new update, big kudos to the team!


@Squaddaffi The issue has been fixed with 0.84.1 release along with a couple other bugs. Let us know if you run into any issues.

Love the idea @Fhireman ! I will be sure to share this with our product team

1 Like

Awesome, thanks for sharing the idea with the product team Ken!
Really appreciate that you guys listen to community suggestions :+1:

I think being able to deal with user sessions will be very welcome for people who use it as a webapp framework.

Each update there are less things left on my wishlist for Streamlit to be the perfect framework for both my own webapp ideas and for it to be interesting for the ML team at my job :smiley:

My ultimate Streamlit wishlist would be:

  • Secure cookies to keep people logged in & set data when reopening the page with their token
  • Streamlit deploy(or serve) CLI command that removes the hamburger menu and running animation(+ stop button) from the top-right so non-technical users just see the page and elements on it without extras(I use Streamlit to help teach Python programming to beginners and it would save them some cognitive complexity as to what’s going on).
  • Image or text alignment arguments e.g. st.image(image, halign=‘center’, valign=‘center’) so layouts don’t seem off when trying to have a centered round image above a piece of text or some radio buttons.
  • A more suitable navigation menu. Maybe a list of stateful buttons instead of radio buttons since they are bigger/easier to click on mobile when navigating to a different “page”. Clicking radio buttons to go to another page doesn’t feel natural to non-technical users(I’ve ran some field tests :stuck_out_tongue:).

Then I’d be one happy Streamlit camper! :partying_face:

Nicetohavesbutnotextremelynecessary would be:

  • Choose where the st.sidebar lives, left or right. A way to make it work horizontally as a navigation bar at the top and bottom would be amazing and opens up a lot of different layout options.
  • Be able to pass and set bg colors and text colors(hex/hsl?) to individual Streamlit widgets would open up creativity on top of the default awesome Streamlit style. (I know about the st.markdown unsafe, allow_html=True workaround but it would be great to have this in a more Streamlitty way like st.button(color=“hsl(0,50,100)”, text_color="#fff", name=“specialbutton”) and link the custom css style to the name value.

Just out of curiosity since I tried it before with the previous hacky sessionstate object and now with the new session state but both without success:
Is it possible to dynamically change the list of radio buttons for navigation after a session state is set?

i.e.: I’ve got a login page on the right, a sidebar on the left with one radio button for navigation named Login. The login page is the default page when someone opens the app.

After successful login a session_state.logged_in gets set and the navigation in the sidebar changes to two buttons (page one, page two) and the page on the right should show the page one widgets(so no more login screen).
When switching to page two it should show page two widgets(here it reverts to the default login radio button again which I didn’t expect since the session_state is set).
The login page and radio button option should be completely gone until the browser refreshes.

So far I’ve gotten half of it working and had some “not iterable” ValueErrors(even when trying to convert to a list after retrieving values from session_state) when using a key=“navoptions” argument with my radiobuttons widget. Where navoptions was set in the session_state to a list with [“pageone”, “pagetwo”].

Any ideas? :grinning:

Hey @Fhireman

I think a small code example that demonstrates this would be easier to debug. I was thinking a key would only be necessary (or perhaps a different key based on the logged in status). The ValueErrors would need to be checked but it’s possible they are triggered because it interprets the value to be “login” but it’s no longer available in the list of options.

Having a code example would be easier to look through.

1 Like

Good news :smiley:

We have something like that planned, which would let you store data per-user in a secure way. Implementation is at least a few quarters away, though.

Something like that is coming soon. Probably by end of year.

This is on our roadmap, but not yet scheduled. We’re still thinking about the best solution here.

Something like that is coming soon. Probably by end of year. Maybe early 2022.


Wow Thiago that’s not only good news, that’s great news! :smiley:
I forgot to add my Flask-style routes wish to the wishlist for different endpoints but the things that you mentioned would already make it so good that I’ll probably only rarely miss it.

Can’t wait to see what awesome stuff is coming up the next quarters! :partying_face:

Edit: I got the dynamic updating to work by moving the selection var/radio buttons menu to the bottom and making it one widget instead of redefining the same widget variable.
Though I still can’t find a way to clear the login screen widgets. It just adds the st.write from pageone to the login page and only removes the login widgets when I switch to pagetwo(going back to pageone after that also works and only shows pageone widgets, it’s just the initial “on submit” on the login screen that doesn’t work as expected).
Is it possible to clear all widgets on successful submit of the form?

Hey Ken,

Of course :slight_smile: without code you don’t know what I’m doing haha.
The code example is a bit of a mess now since I’ve tried everything from saving current selection to state, to trying to clear the login page widgets with a callback and trying different orders, etc, etc.

I’ve gotten to the point that switching to pagetwo and back to pageone after login it ends up the way I want it, but now I just need to get rid of that login radio button on successful login and show the pageone widget.

import streamlit as st

def loginPage():

    test_users = {"test":"test"}

    st.markdown("### Welcome to a demoapp :shopping_bags:")
    st.markdown("### Use the menu on the left(top-left arrow on mobile) to navigate! :smile:")

    with st.form(key='login_form'):
        if "username" not in st.session_state:
            username = st.text_input("Username or e-mail")
            password = st.text_input("Password", type="password")
            username = st.text_input("Username or e-mail")
            password = st.text_input("Password", type="password")
        submit_button = st.form_submit_button(label="Submit")

    if submit_button:
        if username in test_users:
            if test_users[username] == password:
                st.success("Succesfully logged in! :tada:")
                st.session_state.logged_in = True
                st.session_state.navopts = ["pageone", "pagetwo"]
                st.session_state.username = username
                return True
                st.error("Aww something went wrong! Couldn't log you in :cold_sweat:")
                return False
            st.error("Aww something went wrong! Couldn't log you in :cold_sweat:")
            return False

def pageOne():
    st.write("Seems like you've logged in!")

selection = None

if "navopts" not in st.session_state:
    st.session_state.navopts = ["login"]
    selection = st.session_state.navopts

if "logged_in" not in st.session_state:
    selection ="", st.session_state.navopts)
    if loginPage():
        st.session_state.navopts = ["pageone", "pagetwo"]
if "logged_in" in st.session_state and st.session_state.logged_in == True:
    selection ="", st.session_state.navopts)

if selection == "pageone":

Hope it’s readable!