Session State Variables Not Saving Correctly Selectbox vs Multiselect

Summary

I’ve added pagination in my app, and am using st.form() and st.form_submit_button() to pass data to the st.session_state() and navigate to the next page. I’ve noticed that using st.selectbox() will successfully pass the input information to the next page, but st.multiselect() will not, and neither does st.text_input(). My end state will allow the user to select multiple options, hit “Submit” in the form, and then navigate to the next page, and on the next page, the selected options from the st.multiselect will be available.

Steps to reproduce

Code snippet:

import streamlit as st

if 'current_page_num' not in st.session_state:
    st.session_state['current_page_num'] = 0

def process_selected_selected_colleagues(selected_input_option_selected_colleagues):
    st.session_state['selected_colleagues'] = selected_input_option_selected_colleagues

def process_selected_second_form(selected_input_option_second_form):
    st.write(selected_input_option_second_form)

def click_next(selected_input_option_selected_colleagues=None, selected_input_option_second_form=None):
    max_page = 3
    if st.session_state['current_page_num'] + 1 > max_page:
        st.session_state['current_page_num'] = 0
    else:
        if selected_input_option_selected_colleagues is not None:
            process_selected_selected_colleagues(selected_input_option_selected_colleagues)
        elif selected_input_option_second_form is not None:
            process_selected_second_form(selected_input_option_second_form)
        st.session_state['current_page_num'] += 1

def click_prev():
    max_page = 3
    if st.session_state['current_page_num'] - 1 < 0:
        st.session_state['current_page_num'] = max_page
    else:
        st.session_state['current_page_num'] -= 1


def page0():
    st.session_state['current_page_num'] = 0
    if 'input_name' in st.session_state:
        submit_colleagues = st.form('submit_colleages')
        with submit_colleagues:
            submit_colleagues.write('The following people work in your workplace:')
            all_colleages = ['Adam', 'Betsy', 'Colleen', 'David', 'Edward', 'Fiona', 'Gary', 'Hasan', 'Isabelle', 'Jeremiah', 'Kazuki']
            submit_colleagues.write(all_colleages)
            # st.selectbox works as expected
            select_colleagues = st.selectbox('Select Colleagues', options=all_colleages)

            # HELP - st.multiselect(), st.text_input() neither of these pass values to the next page
            #select_colleagues = st.multiselect('Select Colleagues', options=all_colleages)
            colleages_submitted = submit_colleagues.form_submit_button(
                'Submit',
                on_click=click_next,
                args=(select_colleagues, None)
            )

def page1():
    st.session_state['current_page_num'] = 1
    st.write(st.session_state['selected_colleagues'])

sidebar_form = st.sidebar.form('sidebar_form')
with sidebar_form:
    input_name = sidebar_form.text_input('What is your name?')
    input_name_submitted = sidebar_form.form_submit_button('Submit Name')

if input_name_submitted:
    st.session_state['input_name'] = input_name

display_next = True if len(input_name) > 0 and 'selected_colleagues' in st.session_state else False 
display_prev = True if st.session_state['current_page_num'] > 1 else False

_, prev, next, _ = st.columns([5, 0.8, 0.8, 5])
if display_next and input_name_submitted:
    next.button(label='Next', on_click=click_next)
elif not display_next and input_name_submitted:
    next.button(label='Next', on_click=click_next, disabled=True)

if display_prev and input_name_submitted:
    prev.button(label='Prev', on_click=click_prev)
elif not display_prev and input_name_submitted:
    prev.button(label='Prev', on_click=click_prev, disabled=True)

pages = {
    0: page0,
    1: page1
}
pages[st.session_state['current_page_num']]()

You can run this script as is, it should work given the same versions of python / streamlit. As you can see in the page0() function, I have both an st.selectbox() and an st.multiselect(). If you comment out the st.selectbox() line, and instead use the st.multiselect(), you’ll see the misconstrued behavior explained below:

Expected behavior:

After the user selects a number of colleagues via the st.multiselect(), the list of names should be saved into st.session_state[‘selected_colleagues’] , similar to how this happens with the st.selectbox() , and should be printed with page1() runs.

Actual behavior:

When using the st.multiselect(), all values selected by the user are not saved into the st.session_state key ‘selected_colleagues’.

Debug info

  • Streamlit version: 1.20.0
  • Python version: 3.8
  • Using Conda
  • OS version: Windows 10 Enterprise Build 19042.2604
  • Browser version: Microsoft Edge Version 111.0.1661.62 (Official build) (64-bit)

Hi @connorwatson0811,

Passing arguments with callbacks like this inside of a form is tricky. I would recommend avoiding using the callback to set the selected colleagues at all, and instead simply set a key on the selectbox or multiselect to automatically write it to st.session_state.

Here is an example that works, that I think has the desired behavior:

import streamlit as st

if "current_page_num" not in st.session_state:
    st.session_state["current_page_num"] = 0

if "selected_colleagues_form_1" not in st.session_state:
    st.session_state["selected_colleagues_form_1"] = None


def click_next():
    max_page = 3
    if st.session_state["current_page_num"] + 1 > max_page:
        st.session_state["current_page_num"] = 0
    else:
        st.session_state["current_page_num"] += 1


def click_prev():
    max_page = 3
    if st.session_state["current_page_num"] - 1 < 0:
        st.session_state["current_page_num"] = max_page
    else:
        st.session_state["current_page_num"] -= 1


def page0():
    st.session_state["current_page_num"] = 0
    if "input_name" in st.session_state:
        submit_colleagues = st.form("submit_colleages")
        with submit_colleagues:
            submit_colleagues.write("The following people work in your workplace:")
            all_colleages = [
                "Adam",
                "Betsy",
                "Colleen",
                "David",
                "Edward",
                "Fiona",
                "Gary",
                "Hasan",
                "Isabelle",
                "Jeremiah",
                "Kazuki",
            ]
            submit_colleagues.write(all_colleages)
            # st.selectbox works as expected
            # select_colleagues = st.selectbox(
            #    "Select Colleagues", options=all_colleages, key="selected_colleagues"
            # )

            # HELP - st.multiselect(), st.text_input() neither of these pass values to the next page
            st.multiselect(
                "Select Colleagues",
                options=all_colleages,
                key="selected_colleagues_form_1",
            )
            submit_colleagues.form_submit_button("Submit", on_click=click_next)


def page1():
    st.session_state["current_page_num"] = 1
    st.write(st.session_state["selected_colleagues_form_1"])


sidebar_form = st.sidebar.form("sidebar_form")
with sidebar_form:
    input_name = sidebar_form.text_input("What is your name?")
    input_name_submitted = sidebar_form.form_submit_button("Submit Name")

if input_name_submitted:
    st.session_state["input_name"] = input_name

display_next = (
    True if len(input_name) > 0 and "selected_colleagues" in st.session_state else False
)
display_prev = True if st.session_state["current_page_num"] > 1 else False

_, prev, next, _ = st.columns([5, 0.8, 0.8, 5])
if display_next and input_name_submitted:
    next.button(label="Next", on_click=click_next)
elif not display_next and input_name_submitted:
    next.button(label="Next", on_click=click_next, disabled=True)

if display_prev and input_name_submitted:
    prev.button(label="Prev", on_click=click_prev)
elif not display_prev and input_name_submitted:
    prev.button(label="Prev", on_click=click_prev, disabled=True)

pages = {0: page0, 1: page1}
pages[st.session_state["current_page_num"]]()

Hi @blackary , thank you so much, your solution worked greatly for my use case! I understand that this works by using the ‘key’ parameter which saves the user input to session state in combination with the callback (running before streamlit re-runs the script top-to-bottom), but do you have any ideas as to why in the original example, passing args with st.multiselect() didn’t work, but with st.selectbox() it worked? I tried to reference the streamlit components source code and noticed the main difference in the return is that multiselect() returns type List, while selectbox() returns type Optional (any). I will definitely be utilizing key more often in development, but it would be helpful to know if there’s a behind-the-scenes reason for the behavior.

BTW - not even ChatGPT was able to narrow down to utilizing ‘key’!

Also, I understand this is beyond the scope of the additional question but one more related issue… if I add another form to page1() and use that to advance to an additional page, page2(), I notice that the value st.session_state[“selected_colleagues_form_1”] is not carrying over, it seems to be not found in session_state when advancing to next page.

How would this be resolved? The end state would be to have any inputs from earlier pages to carry over into the following pages. Thank you again for your time!

if "selected_colleagues_form_1" not in st.session_state:
    st.session_state["selected_colleagues_form_1"] = None

if "selected_salary_form_2" not in st.session_state:
    st.session_state["selected_salary_form_2"] = None


def page1():
    st.session_state["current_page_num"] = 1
    st.write('page 1')
    st.write(st.session_state["selected_colleagues_form_1"])
    if "input_name" in st.session_state:
        submit_colleagues = st.form("submit_salary")
        with submit_colleagues:
            submit_colleagues.write("The following people work in your workplace:")
            st.number_input(
                "Select salary",
                min_value=50000, max_value=100000,step=1000,value=75000,key='selected_salary_form_2'
            )
            submit_colleagues.form_submit_button("Submit", on_click=click_next)

def page2():
    st.session_state["current_page_num"] = 2
    st.write('page 2')
    st.write(st.session_state["selected_colleagues_form_1"])
    st.write('salary'+ str(st.session_state['selected_salary_form_2']))

not sure if this is the recommended solution but I solved this by making a copy of the session state variable which persists across pages :slight_smile:

def page1():
    st.session_state["current_page_num"] = 1
    st.write('page 1')
    if 'colleague_persist' not in st.session_state:
        st.session_state['colleague_persist'] = st.session_state["selected_colleagues_form_1"]
    st.write(st.session_state["selected_colleagues_form_1"])
    if "input_name" in st.session_state:
        submit_colleagues = st.form("submit_salary")
        with submit_colleagues:
            submit_colleagues.write("The following people work in your workplace:")
            st.number_input(
                "Select salary",
                min_value=50000, max_value=100000,step=1000,value=75000,key='selected_salary_form_2'
            )
            submit_colleagues.form_submit_button("Submit", on_click=click_next)

def page2():
    st.session_state["current_page_num"] = 2
    st.write('page 2')
    st.write(st.session_state["selected_colleagues_form_1"]) #does not persist
    st.write(st.session_state["colleague_persist"]) #persist
    st.write('salary '+ str(st.session_state['selected_salary_form_2']))

Yes, that is probably the best workaround. Right now if you remove a widget from the page, its key also gets removed from session state, UNLESS you explicitly copy it to session state like you did.

Re: Why does this happen – I figured it out. It actually doesn’t work with either selectbox or multiselect. It’s just that selectbox defaults to the first item in the list of options, and multiselect defaults to an empty list. If you notice, even if you select a different name, the selectbox version always returns Adam.

The reason for this (as far as I can tell) is that inside a form, changing a widget doesn’t actually do anything UNTIL you hit the submit button. So, the value passed to args in your submit button ends up always being the default value of your selectbox/multiselect, and NOT the value that you actually selected before submitting the form.

1 Like

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.