streamlit
streamlit copied to clipboard
`st.selectbox` sometimes ignores user input when order of `options` changes
Checklist
- [X] I have searched the existing issues for similar issues.
- [X] I added a very descriptive title to this issue.
- [X] I have provided sufficient information below to help reproduce this issue.
Summary
Hi Streamlit team! I have found a weird edge case where 50% of the time, my st.selectbox user input is ignored, and the other 50% it works as expected.
I have noticed that this happens specifically when I try to change the order of the options value. I have included a code example tested on Streamlit v1.33.0 that shows the issue.
Reproducible Code Example
import streamlit as st
# set up some dummy session state variables
if 'options' not in st.session_state:
st.session_state.options = ['D', 'C', 'B', 'A', 'F', 'E']
st.session_state.selected_option = st.selectbox(
label='Select an option',
options=st.session_state.options,
)
st.markdown(f'You selected: {st.session_state.selected_option}')
# move the selected option to the front of the list if it is not already
if st.session_state.selected_option != st.session_state.options[0]:
st.session_state.options.remove(st.session_state.selected_option)
st.session_state.options.insert(0, st.session_state.selected_option)
Steps To Reproduce
You should be able to run the above code and see the issue as shown in the attached GIF!
Expected Behavior
Ideally, when the user selects another option, we always respect that as the truth and show that option in the rendered select box.
Current Behavior
Is this a regression?
- [ ] Yes, this used to work in a previous version.
Debug info
- Streamlit version: 1.33.0
- Python version: 3.10.14
- Operating System: MacOS running an Ubuntu Docker container
- Browser: Chrome
Additional Information
No response
If this issue affects you, please react with a 👍 (thumbs up emoji) to the initial post.
Your feedback helps us prioritize which bugs to investigate and address first.
@nathancooperjones this is unfortunately less of a "bug" and more an intended but unexpected consequence of the way the streamlit execution model and widget system works. Any time you change the input value of an argument to a widget, streamlit sort of perceived that widget as an entirely new widget, and doesn't guarantee that the old widget value will carry over. There are a couple of technical reasons why it has to be this way, but that's partly what's causing the problem here. There's a "careful dance of information" (for lack of a better term) going on between the frontend webpage and the python backend each run and "new" widgets are treated slightly differently from ones that existed during the last script run. See https://docs.streamlit.io/develop/concepts/architecture/widget-behavior for a little more info.
The other issue I see here is that you're modifying the order of the list after the widget is rendered, which means it doesn't take effect until the next time the script reruns.
Try placing your if statement that reorders the list inside a function that is called by st.selectbox on_change and see if you get more reliable results.
@nathancooperjones thanks for reporting this 👍 I reproduced it here. As @Asaurus1 explained correctly, its a behaviour that's currently expected (-> changing this to a feature request). But we are aware that it is causing trouble in some usecases.
Related issues for other widgets are:
- https://github.com/streamlit/streamlit/issues/7749
- https://github.com/streamlit/streamlit/issues/7855
- https://github.com/streamlit/streamlit/issues/8410
- https://github.com/streamlit/streamlit/issues/4854 (I believe this is probably a bit overlapping with this issue)
is there any workaround to fix this issue
@Asaurus1 @LukasMasuch thanks for the reply and linking some similar issues, I appreciate the response! I tried moving the option sorting logic into a function called by the selectbox's on_change but the issue still happens :(
As a workaround in the meantime, I've been caching the options sent into the selectbox so the widget doesn't reset each time until the set of options actually change. It's a SUPER hacky, but it works well-enough for my use case:
import streamlit as st
# set up some dummy session state variables
if 'options' not in st.session_state:
st.session_state.options = ['D', 'C', 'B', 'A', 'F', 'E']
# cache the options to the ``st.selectbox`` doesn't reset and ignore a user's input if the order of
# the options changes
if set(st.session_state.get('_cached_options', [])) != set(st.session_state.options):
st.session_state._cached_options = sorted(st.session_state.options)
if 'selected_option' in st.session_state:
st.session_state._selectbox_index = (
st.session_state._cached_options.index(st.session_state.selected_option)
)
else:
st.session_state._selectbox_index = 0
st.session_state.selected_option = st.selectbox(
label='Select an option',
options=st.session_state._cached_options,
index=st.session_state._selectbox_index,
)
st.markdown(f'You selected: {st.session_state.selected_option}')
# move the selected option to the front of the list if it is not already
if st.session_state.selected_option != st.session_state.options[0]:
st.session_state.options.remove(st.session_state.selected_option)
st.session_state.options.insert(0, st.session_state.selected_option)
# dummy logic to add an option just to see how ``st.session_state.options`` changes
if st.button('Add an option'):
option_to_add = 1
while True:
if str(option_to_add) not in st.session_state.options:
st.session_state.options.append(str(option_to_add))
st.rerun()
option_to_add += 1
@nathancooperjones hmm, I think that might be a bit overkill. This should be what you want
import streamlit as st
# set up some dummy session state variables
st.session_state.setdefault("options", ['D', 'C', 'B', 'A', 'F', 'E'])
def update_options():
# move the selected option to the front of the list if it is not already
if st.session_state.selected_option != st.session_state.options[0]:
st.session_state.options.remove(st.session_state.selected_option)
st.session_state.options.insert(0, st.session_state.selected_option)
st.selectbox(
label='Select an option',
options=st.session_state.options,
key="selected_option",
on_change=update_options,
)
st.markdown(f'You selected: {st.session_state.selected_option}')
Here's what's happening:
- You create the options list in session_state
- You create a selectbox that is tied to the selected_option key
- When the selection changes, the update_options function is run before your script code and updates the list in session state, causing the order to change.
@nathancooperjones hmm, I think that might be a bit overkill. This should be what you want
OMG this is SO much better than the solution I had, thank you so much!!
I realized that it didn't work for me because I was assigning the output of selectbox to the variable. This does not work:
import streamlit as st
# set up some dummy session state variables
if 'options' not in st.session_state:
st.session_state.options = ['D', 'C', 'B', 'A', 'F', 'E']
def update_options():
# move the selected option to the front of the list if it is not already
if st.session_state.selected_option != st.session_state.options[0]:
st.session_state.options.remove(st.session_state.selected_option)
st.session_state.options.insert(0, st.session_state.selected_option)
st.session_state.selected_option = st.selectbox(
label='Select an option',
options=st.session_state.options,
on_change=update_options,
)
st.markdown(f'You selected: {st.session_state.selected_option}')
But this does:
import streamlit as st
# set up some dummy session state variables
if 'options' not in st.session_state:
st.session_state.options = ['D', 'C', 'B', 'A', 'F', 'E']
def update_options():
# move the selected option to the front of the list if it is not already
if st.session_state.selected_option != st.session_state.options[0]:
st.session_state.options.remove(st.session_state.selected_option)
st.session_state.options.insert(0, st.session_state.selected_option)
st.selectbox(
label='Select an option',
options=st.session_state.options,
key='selected_option', # change here
on_change=update_options,
)
st.markdown(f'You selected: {st.session_state.selected_option}')
This is super good to know going forward, thank you!!
Right, the "key" here is that you give the widget a "key" argument (pun intended) so you can extract the selected value from within the update_options callback. Otherwise that variable doesn't get updated until the next run of the script reaches the selectbox line, which is already too late.
On Apr 22, 2024 at 08:33, Nate Jones @.***> wrote:
@nathancooperjones hmm, I think that might be a bit overkill. This should be what you want
OMG this is SO much better than the solution I had, thank you so much!!
I realized that it didn't work for me because I was assigning the output of selectbox to the variable. This does not work:
import streamlit as st # set up some dummy session state variables if 'options' not in st.session_state: st.session_state.options = ['D', 'C', 'B', 'A', 'F', 'E'] def update_options(): # move the selected option to the front of the list if it is not already if st.session_state.selected_option != st.session_state.options[0]: st.session_state.options.remove(st.session_state.selected_option) st.session_state.options.insert(0, st.session_state.selected_option) st.session_state.selected_option = st.selectbox( label='Select an option', options=st.session_state.options, on_change=update_options, ) st.markdown(f'You selected: {st.session_state.selected_option}')
But this does:
import streamlit as st # set up some dummy session state variables if 'options' not in st.session_state: st.session_state.options = ['D', 'C', 'B', 'A', 'F', 'E'] def update_options(): # move the selected option to the front of the list if it is not already if st.session_state.selected_option != st.session_state.options[0]: st.session_state.options.remove(st.session_state.selected_option) st.session_state.options.insert(0, st.session_state.selected_option) st.selectbox( label='Select an option', options=st.session_state.options, key='selected_option', # change here on_change=update_options, ) st.markdown(f'You selected: {st.session_state.selected_option}')
This is super good to know going forward, thank you!!
— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.Message ID: @.***>
I encounter this problem, with st.selectbox reseting, only when I use st.navigation with "pages" folder (the old way) this problem does not manifest.
This issue (intended behavior) is present when setting default through index or value. My use case for "dynamic defaults" is for this is session persistency and stateful shareable links.
Below are two examples, first time value changes it works second time it doesn't third time it does etc.
Streamlit version: 1.38.0
import streamlit as st
if 'app' not in st.session_state:
st.session_state['app'] = 'Home'
options = ['Home', 'Page 1', 'Page 2']
st.session_state['app'] = st.selectbox(
'Select a page',
options,
index = options.index(st.session_state['app'])
)
st.write(st.session_state['app'])
# With
if 'text' not in st.session_state:
st.session_state['text'] = 'Hello World'
st.session_state['text'] = st.text_input('Enter some text', value = st.session_state['text'])
st.write(st.session_state['text'])
@ProkhorZakharov yes, the behavior you're getting is simply a byproduct of streamlit's execution model. The "streamlit way" to do what you are attempting is to assign a "key=" to each of the widgets. This key will then automatically be included in the session state as a variable and you can modify it to change the widget's value on-the-fly.