Session_state seems to be dropping some data

Summary

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.

image

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)

Ah, thanks @mathcatsand … got it.

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.

What am I missing?

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)

Thanks very much @mathcatsand

1 Like

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]]

  1. Store information using the widget’s key
  2. Run the callback
  3. 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)
1 Like

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.

I hope this is documented somewhere clearly.

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:

  1. Save information into a different key in session state not associated to a widget so it is protected from deletion.
  2. 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.

2 Likes

Thanks @mathcatsand !

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.

I hope Streamlit will take note of this issue!

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.

1 Like

Glad to know that it’s being handled!

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.

Thanks for pointing me in the right direction, though!

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.

I understand. Option 1 is my preferred method as well. I was just explaining what is possible. :slight_smile:

1 Like

Hello!

Option 1 would work well, but it seems to then require double interaction from the user with the widget to get the correct value to be set. I’ve tried updating the state with a callback, but I cannot pass the widget’s selected value in kwargs…

If anyone has a simple example of 1 widget’s state (say, a multiselect) being saved across pages, without requiring double interaction from users in order to update correctly, that would be great.

You can’t pass a widget’s value directly in a callback, but the widget’s value can be accessed within the callback using its key with a call to Session State.

You can either have a callback which hardcodes the key it needs to copy from, or you can pass the key itself into a callback that you use on multiple widgets. I do something like that here.