Inconsistent behaviour of st.selectbox with sessions

Hello, I’m fairly new to streamlit and have been trying out some features to develop an app.
Ran into a problem recently with session states. It is related to the behavior of st.selectbox (selection getting reset to the first option) when the list of options used in the parameter changes based on another selection in the app.
Related thread here

I found a workaround for this using the session state variable along with the index param of st.selectbox.
refer to the following simplified example code which shows the same behavior

import pandas as pd
import streamlit as st


def main():
    # create two dataframes
    df1_dict = {"dropdown_cols": ["1", "2", "3", "4", "5"]}
    df1 = pd.DataFrame(df1_dict)
    df2_dict = {"dropdown_cols": ["1", "2", "3", "4", "5", "6"]}
    df2 = pd.DataFrame(df2_dict)

    selected_df = st.selectbox(
                "Select dataframe to use",
                ["df1", "df2"],
            )

    df = df1 if selected_df == "df1" else df2

    # the following list changes based on what is selected in the above selectbox
    options = df["dropdown_cols"].tolist()

    # When the session variable is not initialized
    if "selected_number" not in st.session_state:
        print("initializing session var 'selected_number'")
        st.session_state["selected_number"] = "1"

    # Check if value exists in the new options list. if it does retain the selection, else reset
    if st.session_state["selected_number"] not in options:
        st.session_state["selected_number"] = options[0]

    prev_num = st.session_state["selected_number"]

    st.session_state["selected_number"] = st.selectbox(
                "select a number",
                options,
                index=options.index(st.session_state["selected_number"])
            )

    number_select = st.session_state["selected_number"]
    # See the change in the selection
    st.write(f"selection changed from {prev_num} to {number_select}")
    print(f"{prev_num} to {number_select}")


if __name__=="__main__":
    main()

Problem
while this implementation does manage to retain the selection when the new list contains the previous selected value, the behavior of the second selectbox seems to be highly erratic as sometimes when an option is selected the selection changes for a fraction of a second and goes back to the previous selection.

Note: this does not always happen and it’s unclear to me as to what is causing this. The cli log does not show any exceptions or errors.
I am printing a statement everytime the session var is being initialized (when it isn’t present in st.session_state) and i can notice that it is being initliazed multiple times.

I have tried using the ‘key’ param but it doesn’t deliver the same functionality.
I am unsure if the problem lies within my implemetation or if i’m missing something,

Any help is appreciated as this makes the app look really buggy.

Thank you.

3 Likes

Hi! Welcome to Streamlit!

I have found that using the widget keys (which are automatically added to session state) and using widget callbacks is a much more stable way to get things done.
I think the code below works… Please test and let me know if there are issues.
Cheers.


import pandas as pd
import streamlit as st


def main():
    # create two dataframes
    df1_dict = {"dropdown_cols": ["1", "2", "3", "4", "5"]}
    df1 = pd.DataFrame(df1_dict)
    df2_dict = {"dropdown_cols": ["1", "2", "3", "4", "5", "6"]}
    df2 = pd.DataFrame(df2_dict)

    st.selectbox(
                "Select dataframe to use",
                ["df1", "df2"], key = 'selected_df'
            )

    df = df1 if st.session_state.selected_df == "df1" else df2

    # the following list changes based on what is selected in the above selectbox
    options = df["dropdown_cols"].tolist()

    # Check if session state object exists
    if "selected_number" not in st.session_state:
        st.session_state['selected_number'] = options[0]
    if 'old_number' not in st.session_state:
        st.session_state['old_number'] = ""    

    # Check if value exists in the new options list. if it does retain the selection, else reset
    if st.session_state["selected_number"] not in options:
        st.session_state["selected_number"] = options[0]

    prev_num = st.session_state["selected_number"]


    def number_callback():
        st.session_state["old_number"] = st.session_state["selected_number"]
        st.session_state["selected_number"] = st.session_state.new_number
        

    st.session_state["selected_number"] = st.selectbox(
                "select a number",
                options,
                index=options.index(st.session_state["selected_number"]),
                key = 'new_number',
                on_change = number_callback
            )

    
    # See the change in the selection
    st.write(f"selection changed from {st.session_state['old_number']} to {st.session_state['selected_number']}")
    print(f"{st.session_state['old_number']} to {st.session_state['selected_number']}")


if __name__=="__main__":
    main()
3 Likes

Hi Luke!

Thanks a lot! your implementation of the same seems to work flawlessly.
I do have a few doubts, I see that you have used both key and index with two different sessions variables, I was wondering how the two contribute in the flow.

I understand the index helps in setting the preselected value when the page loads, but am i right in assuming that here the session variable used for the ‘key’ param will have the newly selected value ?

And could you point out why the selectbox was behaving in such an unpredictable manner in my initial method?

I am just trying to understand so that i can implement sessions in a better way in the future.

Thank you!

Hi,
I wrote that just before bed last night so there may have been some bugs. I’m also not a super programmer (I am a data analyst) so I’m not sure I can fully explain everything.

I guess the point is that the “key” value is what is currently selected and the “selected_number” is just a placeholder for that key. I think you still need the session state variable “selected_number” or it throws an error the first time the app is run, since the key is not yet in session state.

I understand the index helps in setting the preselected value when the page loads, but am i right in assuming that here the session variable used for the ‘key’ param will have the newly selected value ?

Yes, I think so, but you need the “selected_number” for the first time the widget loads.

And could you point out why the selectbox was behaving in such an unpredictable manner in my initial method?

To be honest, I’m not sure. Hopefully someone here can point it out :–)

Hi,
Thanks for the insight. Helps a lot. :slight_smile:
I did test the program you wrote and it seems to be working as i expected it to.

1 Like

In case you’re interested I actually wrote a mini tutorial yesterday on how to solve similar issues.

1 Like

Thank you! I’m sure this will help a lot of people having trouble with managing sessions.

Hi everyone,

Did anyone figure out why this inconsistent behavior happens sometimes? I still experience this with Streamlit 1.17.0.

Can you give a small, reproducible example of your current problem? Looking briefly at the original post, I think it is describing expected behavior: if you change the the list of options for the widget, Streamlit will view it as a new instance and create the widget from scratch (e.g. back to the default value which is the first item in the options list unless otherwise specified for a select box). Unless you do something with setting the initial value or managing the key, a change in any parameter of the select box will cause it to reset. You can certainly get around this a couple different ways, but if you share what your current use case is, we can more concretely clarify whatever is troubling you.

1 Like

This is how I solved it. The comments explain the rationale! Cheers!

def YourOption_select():
    OptionDefaultIndex = 0

    if 'YourOptionSelectButton_State' in st.session_state:
        #
        # YourOptionSelectButton_State : This is the widget key
        #
        # When user selects an option, the "widget key" in st.session_state is already populated with the option
        # So we just find the option index and pass it while rendering the selectBox
        #
        try:
            OptionDefaultIndex = GetYourOptions().index(st.session_state['YourOptionSelectButton_State'])
        except:
            OptionDefaultIndex = 0
    elif 'YourOptionSelectButton_State_Backup' in st.session_state:
        #
        # Sometimes, when we stray out of a page to another page that does not have this widget,
        # then streamlit erases the "widget key" from the state. 
        # When we return back, we find that streamlit has forgotten our selection.
        # So to handle that case, we use the Backup to provide for the right selection option.
        # NOTE: Streamlit does not delete user created keys unlike the widget keys
        #
        try:
            OptionDefaultIndex = GetYourOptions().index(st.session_state['YourOptionSelectButton_State_Backup'])
        except:
            OptionDefaultIndex = 0
    else:
        OptionDefaultIndex = 0

    SelectedOption = st.selectbox(
                        'Select YourOption', 
                        GetYourOptions(), #This call should return same sequence of items every time
                        index=OptionDefaultIndex,
                        key='YourOptionSelectButton_State'
                    )
    
    st.session_state['YourOptionSelectButton_State_Backup'] = SelectedOption
    return SelectedOption

Any joy with this?
I have a similar problem as the OP.
I could try using the callbacks - but I shouldnt have to.
My selectbox consistently bounces back to the selected value when selecting a new one. Print and debug show me that it is not loading the page or sidebar again - but visually I can see it taking the new selection then switching back to teh original after a fraction of a section.

unique widget keys do not help.

@rhubarb

Another user bitten by selectbox behavior.
So, I wrote a function that works this around cleanly.
This gist should help.

Thanks, that helped a lot.
Here is a “one liner” which could be simpler to use:

        name = "selection_box1"
        st.session_state[name] = st.selectbox('YourSelectBoxLabel',
                                              options=options,
                                              index=options.index(st.session_state.get(name, options[0])),
                                              on_change=lambda: st.session_state.update({name: st.session_state[name + "_key"]}),
                                              key=name + "_key")
2 Likes