Iâm trying out session_state in a simple test case. Itâs largely working, but on one particular action it clears one of its attributes, and I canât see why.
Steps to reproduce
Code snippet:
import streamlit as st
st.session_state
if 'running_query' not in st.session_state:
st.session_state.running_query = False
if 'prefix' not in st.session_state:
st.session_state.prefix = 'ABC'
st.session_state
def run_query():
st.session_state.running_query = True
def change_prefix():
st.session_state.running_query = False
if not st.session_state.running_query:
st.write("Not running query")
title = st.text_input('Code prefix:', key='prefix')
st.button('Run query', on_click=run_query)
else:
st.write('Running query with:', st.session_state.prefix)
st.button('Change prefix', on_click=change_prefix)
If applicable, please provide the steps we should take to reproduce the error or specified behavior.
Expected behavior:
I should be able to set the prefix by typing âtestâ in the input box to replace the default âABCâ, hit enter to apply, then ârun queryâ and see a message saying âRunning query with testâ.
Then click âchange prefixâ and the default in the Code prefix input box should be âtestâ.
Actual behavior:
When clicking âchange prefixâ in the final step described above, instead the default goes back to âABCâ.
Debug info
Streamlit version: 1.14.0
Python version: 3.9.13
Using Conda
OS version: macOS 13.0.1
Browser version: Chrome Version 107.0.5304.110 (Official Build) (arm64)pytho
Requirements file
name: streamlit
channels:
defaults
conda-forge
dependencies:
pip
matplotlib=3.5.0
numpy=1.21.2
plotly=5.6.0
python=3.9.13
pip:
pandas==1.5.0
pip==22.2.2
pyathena==2.5.1
sqlalchemy==1.4.32
streamlit==1.14.0
toml==0.10.2
Additional information
I put in the two magic calls to display st.session_state and it seems clear that when the âchange prefixâ button is pressed, the session_state data has been lost - running_query is set to False but prefix is nowhere to be seen, so the default init statement sets it back to ABC. Why is this happening? I want the input box to be pre-populated with whatever the user put in last time, so they can edit it.
To be clear, this is just a test rig for me to understand session_state, so Iâm not looking for workarounds - I want to understand why itâs not holding on to the state as I think it should be?
Your code is updating the prefix fine for me. Are you hitting enter to submit the text before clicking the Run query button? If you type in text but click the button before submitting the text (with enter), it wonât register the text there.
You can use a form to get around the need to âsubmit the textâ before clicking the button the run the query.
import streamlit as st
st.session_state
if 'running_query' not in st.session_state:
st.session_state.running_query = False
if 'prefix' not in st.session_state:
st.session_state.prefix = 'ABC'
st.session_state
def run_query():
st.session_state.running_query = True
def change_prefix():
st.session_state.running_query = False
change_state = st.form(key='my_form')
with change_state:
if not st.session_state.running_query:
st.write("Not running query")
title = st.text_input('Code prefix:', key='prefix')
st.form_submit_button('Run query', on_click=run_query)
else:
st.write('Running query with:', st.session_state.prefix)
st.form_submit_button('Change prefix', on_click=change_prefix)
Strange⌠yes, I am pressing enter to apply the input. It does indeed pick up my input in the next stage (showing âRunning query with: testâ or whatever Iâve entered) - this isnât where Iâm seeing a problem.
The problem comes when returning to edit the prefix by clicking on the âchange prefixâ at this point, which then always gives me the ABC default in the input box, rather than âtestâ or whatever I was most recently using.
To be clear, what I would expect to happen when pressing âchange prefixâ is that session_state.prefix retains its value. But instead it gets dropped and reset to the default.
Can you confirm @mathcatsand that you were seeing the correct behaviour after clicking âchange prefixâ? In which case thatâs weird, given the same setup and the same codeâŚ
Btw, Iâve tried changing to use a form as you suggest (v helpful hint - thanks) but it doesnât make any difference to this session_state issue.
I see your question. My understanding is that since the key is associated with a widget (the gets destroyed), it is getting removed from session_state. You can buffer that by copying it over to a different key that Streamlit wonât delete (by way of association to a disappearing input), or utilize the fact the input box is getting destroyed and use value to set a default from something kept in session_state.
Hereâs the first suggestion as an example, with an extra print of session_state so you can see what happens from the callback running to the next page load.
import streamlit as st
st.session_state
if 'running_query' not in st.session_state:
st.session_state.running_query = False
if 'prefix' not in st.session_state:
st.session_state.prefix = 'ABC'
if 'prefix_memory' not in st.session_state:
st.session_state.prefix_memory = 'ABC'
st.session_state
def run_query():
st.session_state
st.write('^^ This is run_query running, just before page reload.')
# This extracts the prefixs into a different slot in session state before
# page reload so it can survive the destruction of the input widget which
# will be gone on next page load.
st.session_state.prefix_memory = st.session_state.prefix
st.session_state.running_query = True
def change_prefix():
st.session_state
st.write('^^ This is change_prefix running, just before page reload.')
st.session_state.running_query = False
change_state = st.form(key='my_form')
with change_state:
if not st.session_state.running_query:
st.write("Not running query")
title = st.text_input('Code prefix:', key='prefix', value=st.session_state.prefix_memory)
st.form_submit_button('Run query', on_click=run_query)
else:
st.write('Running query with:', st.session_state.prefix)
st.form_submit_button('Change prefix', on_click=change_prefix)
Seems strange behaviour though, that using the session_state property (prefix here) as the key to the widget is what the documentation seems to recommend, but I canât see it anywhere that it makes mention of the fact that it gets dropped when the widget goes.
You suggest two methods - the first, as per your code, makes sense. But Iâd like to understand the second option too, and canât get it to work. Sorry - feel pretty stupid coming back again with such a basic problem, but I donât seem to have got my head round how streamlit handles these inputs.
Hereâs what Iâve done to try to implement your option 2:
import streamlit as st
st.session_state
if 'running_query' not in st.session_state:
st.session_state.running_query = False
if 'prefix' not in st.session_state:
st.session_state.prefix = 'ABC'
st.session_state
def run_query(prefix):
st.session_state.running_query = True
st.session_state.prefix = prefix
def change_prefix():
st.session_state.running_query = False
change_state = st.form(key='my_form')
with change_state:
if not st.session_state.running_query:
st.write("Not running query")
edited_prefix = st.text_input('Code prefix', value=st.session_state.prefix)
st.form_submit_button('Run query', on_click=run_query, args=(edited_prefix,))
else:
st.write('Running query with:', st.session_state.prefix)
st.form_submit_button('Change prefix', on_click=change_prefix)
So this gives me a different problem: the edited prefix is never picked up, so whatever I type in the input box, the parameter supplied to the run_query function is always âABCâ and the prefix doesnât change.
Sorry, I think I mashed the two solutions a bit from playing around with the code. To clarify, hereâs two (slightly) different solutions:
import streamlit as st
st.session_state
if 'running_query' not in st.session_state:
st.session_state.running_query = False
if 'prefix_memory' not in st.session_state:
st.session_state.prefix_memory = 'ABC'
st.session_state
def run_query():
st.session_state.prefix_memory = st.session_state.prefix
st.session_state.running_query = True
def change_prefix():
st.session_state.running_query = False
change_state = st.form(key='my_form')
with change_state:
if not st.session_state.running_query:
st.write("Not running query")
# We don't do anything manual to assign value to st.session_state.prefix
# We are only using it as a key to access the information during the callback.
# The prefix key goes away when the text input widget goes away,
# so this widget is recreated each time it shows up
title = st.text_input('Code prefix:', key='prefix', value=st.session_state.prefix_memory)
st.form_submit_button('Run query', on_click=run_query)
else:
st.write('Running query with:', st.session_state.prefix)
st.form_submit_button('Change prefix', on_click=change_prefix)
import streamlit as st
st.session_state
if 'running_query' not in st.session_state:
st.session_state.running_query = False
if 'prefix_memory' not in st.session_state:
st.session_state.prefix_memory = 'ABC'
# Here we protect against the destruction by re-initializing from prefix_memory
if 'prefix' not in st.session_state:
st.session_state.prefix = st.session_state.prefix_memory
st.session_state
def run_query():
st.session_state.prefix_memory = st.session_state.prefix
st.session_state.running_query = True
def change_prefix():
st.session_state.running_query = False
change_state = st.form(key='my_form')
with change_state:
if not st.session_state.running_query:
st.write("Not running query")
title = st.text_input('Code prefix:', key='prefix')
st.form_submit_button('Run query', on_click=run_query)
else:
st.write('Running query with:', st.session_state.prefix)
st.form_submit_button('Change prefix', on_click=change_prefix)
And just to clarify a bit on what you were working with, in this snippet, edited_prefix will get the value from what was in the input box on page load, not from what was just submitted. Hence, the callback function will never see the ânewâ entry. If you need to access a new submission within a callback, you have to do it from st.session_state.<widget key> since the order of operations is
[[0. If a widget is set to âoutputâ to a variable, it gets the pre-existing/default value on page load]]
Store information using the widgetâs key
Run the callback
Reload the page at which point the widget will âoutputâ to any variable assignment based on the new page load (if the widget persisted, it will remember the last submission)
I faced this problem too â when navigating across multiple pages (âsettingsâ page and ârunâ page), the slider value gets lost on the second visit to the settings page. So I had to back up the session value into a separate session value.
Also, on the second visit, the widgetâs value (slider) gets lost so I also needed to restore the value for the widget â so at minimum, both backup and restore were needed as follows:
If there are better ways Iâd like to learn.
# Backup
def on_change():
st.session_state.value = st.session_state.value_widget
# Restore
if 'value_widget' not in st.session_state:
st.session_state.value_widget = st.session_state.value
st.slider('Value', min_value=0.0, max_value=1.0, step=0.01, key='value_widget', on_change=on_change)
If a key is tied to a widget, it will get deleted from session state if that widget disappears (whether conditionally not rendered on the same page or from navigating to a different page). If you have two widgets on two different pages with the same parameters/key, Streamlit will still recognize them as different widgets so you will in effect get the key deleted and a new one created on the same page load if you switched pages with an attempt to connect it up to a new widget.
There are two solutions to this:
Save information into a different key in session state not associated to a widget so it is protected from deletion.
Recommit widget keys you want to persist across pages at the top of every page:
#list of keys tied to widgets that you want to protect
keeper_list = ['widget_key_1', 'widget_key_2']
for key in keeper_list:
if key in st.session_state:
st.session_state[key] = st.session_state[key]
What this trivial assignment does is interrupt the widget cleanup process, thereby protecting it from getting deleted.
Unfortunately the option 2 wonât work if you have 2+ pages â the key gets deleted when you visit other pages, so the keeper code needs to be placed on every page, which is significantly more complex than option 1.
See this thread and the associated GitHub issue. This is being looked at, but itâs not yet decided what the solution will be.
For the pages, can you try adding st.session_state.update(st.session_state) on top of the page? This should take the not-yet-cleared widget state and save it in session_state.
This wonât work either because it needs to be put on top of every page, and then it will raise StreamlitAPIException for widgets/forms on other pages.
Can you provide some code for solution 2 not working? There are many threads discussing this and it has historically worked.
Copying all of session state may raise an issue as you indicated when you include keys for widgets that donât accept manual assignment of value. Thatâs why you (in general) need a short list of just the widget values you want to preserve.
Copy this code to two different pages and you can see that state is preserved.
import streamlit as st
keeper = ['test', 'key']
for key in keeper:
if key in st.session_state:
st.session_state[key] = st.session_state[key]
if st.checkbox('test', key='test'):
st.slider('Keyed Widget',0,10,1, key='key')
The key deletion does not happen before the new page starts running, which is why this works. However, there may be some edge cases involving containers/empty that might trigger some other consideration, so please do share if you have an example.
Edit: Maybe I misunderstood your post on second reading. Yes, the keeper workaround is needed on every page to work, even the ones not containing the related widgets.
It works, but the problem is that you need to copy-paste those 6 lines in ALL pages. It is not DRY and more prone to error, especially when you have many pages to maintain.
The backup-restore hack in OP is less kludgy as those 4 lines are only put on the SINGLE page which is directly related to the widget on the page, therefore better for separation of concerns.