pythonsession-statestreamlit

Session state is reset in Streamlit multipage app


I'm building a Streamlit multipage application and am having trouble keeping session state when switching between pages. My main page is called mainpage.py and has something like the following:

import streamlit as st

if "multi_select" not in st.session_state:
    st.session_state["multi_select"] = ["abc", "xyz"]
if "select_slider" not in st.session_state:
    st.session_state["select_slider"] = ("1", "10")
if "text_inp" not in st.session_state:
    st.session_state["text_inp"] = ""

st.sidebar.multiselect(
    "multiselect",
    ["abc", "xyz"],
    key="multi_select",
    default=st.session_state["multi_select"],
)

st.sidebar.select_slider(
    "number range",
    options=[str(n) for n in range(1, 11)],
    key="select_slider",
    value=st.session_state["select_slider"],
)
st.sidebar.text_input("Text:", key="text_inp")

for v in st.session_state:
    st.write(v, st.session_state[v])

Next, I have another page called 'anotherpage.py' in a subdirectory called 'pages' with this content:

import streamlit as st

for v in st.session_state:
    st.write(v, st.session_state[v])

If I run this app, change the values of the controls and switch to the other page, I see the values for the control being retained and printed. However, if I switch back to the main page, everything gets reset to the original values. For some reason st.session_state is cleared.

Anyone have any idea how to keep the values in the session state? I'm using Python 3.11.1 and Streamlit 1.16.0


Solution

  • First, it's important to understand a widget's lifecycle. When you assign a key to a widget, then that key will get deleted from session state whenever that widget is not rendered. This can happen if a widget is conditionally not rendered on the same page or from switching pages.

    What you are seeing on the second page are the values leftover from the previous page before the widget cleanup process is completed. At the end of loading "anotherpage," Streamlit realizes it has keys in session state assigned to widgets that have disappeared and therefore deletes them.

    There are two ways around this.

    1. A hacky solution (not my preference) is to recommit values to session state at the top of every page.
    st.session_state.my_widget_key = st.session_state.my_widget_key
    

    This will interrupt the widget cleanup process and prevent the keys from being deleted. However, it needs to be on the page you go to when leaving a widget. Hence, it needs to be on all the pages.

    1. My preferred solution is to think of widget keys as separate from the values I want to keep around. I usually adopt the convention of prefixing widget keys with an underscore.
    import streamlit as st
    
    if "multi_select" not in st.session_state:
        st.session_state["multi_select"] = ["abc", "xyz"]
    if "select_slider" not in st.session_state:
        st.session_state["select_slider"] = ("1","10")
    if "text_inp" not in st.session_state:
        st.session_state["text_inp"] = ""
    
    def keep(key):
        # Copy from temporary widget key to permanent key
        st.session_state[key] = st.session_state['_'+key]
    
    def unkeep(key):
        # Copy from permanent key to temporary widget key
        st.session_state['_'+key] = st.session_state[key]
    
    unkeep("multi_select")
    st.sidebar.multiselect(
        "multiselect",
        ["abc", "xyz"],
        key="_multi_select",
        on_change=keep,
        args=['multi_select']
    )
    
    # This is a edge case and possibly a bug. See explanation.
    st.sidebar.select_slider(
        "number range",
        options=[str(n) for n in range(1, 11)],
        value = st.session_state.select_slider,
        key="_select_slider",
        on_change=keep,
        args=["select_slider"]
    )
    
    unkeep("text_inp")
    st.sidebar.text_input("Text:", key="_text_inp", on_change=keep, args=["text_inp"])
    
    for v in st.session_state:
        st.write(v, st.session_state[v])
    

    You will observe I did something different with the select slider. It appears a tuple needs to be passed to the value kwarg specifically to make sure it initializes as a ranged slider. I wouldn't have needed to change the logic if it was being initialized with a single value instead of a ranged value. For other widgets, you can see that the default value is removed in favor of directly controlling their value via their key in session state.

    You need to be careful when you do something that changes a widget's default value. A change to the default value creates a "new widget." If you are simultaneously changing the default value and actual value via its key, you can get some nuanced behavior like initialization warnings if there is ever a conflict.