St.form and script re-runs

Hi there,

I would like to guide a user through a multi-step submission process and I am noticing some quirky interactions between st.form and st.session_state.

The two problems I am having are illustrated in the reprex below.

Problem 1

The st.session_state key is lost if an associated widget disappears. This problem has been known for a while and a workaround is implemented below, but I am still curious if this can be fixed in some other way?

Problem 2

This is the main thing: with Streamlit’s re-run model, I would expect the following behaviour:

  1. First form input is isolated from the re-runs; the user can explore various options.
  2. Once the first st.form_submit_button is clicked, st.session_state["step"] is set to 2.
  3. A re-run is triggered, causing the second form to be displayed.

Instead, submitting the first form sets the new st.session_state["step"] = 2, but the first form button needs to be clicked a second time in order to display the second form.

Can someone help me understand what is happening here? Thanks!

Reprex code

import streamlit as st

def init():
    if "init" not in st.session_state:
        st.session_state["init"] = True
        st.session_state["step"] = 1
        st.session_state["animal"] = None
        st.session_state["color"] = None

def choose_animal():
    with st.form(key="form_animal", clear_on_submit=False):
        st.header("Step 1")

        # Problem 1: this way of setting state is lost when widget disappears
        #animal = st.selectbox(label="Choose your favorite animal", key="animal",
        #                      options=["Cat", "Dog", "Zebrafish"])

        # In order to preserve state after widget disappears,
        # st.session_state needs to be manually set to widget value
        animal = st.selectbox(label="Choose your favorite animal",
                              options=["Cat", "Dog", "Zebrafish"])
        if st.form_submit_button("Confirm choice"):
            st.session_state["animal"] = animal
        # Problem 2: the session state is set here
        # but the script does not re-run until the button is clicked a second time
            st.session_state["step"] = 2

def choose_color():
    with st.form(key="form_color", clear_on_submit=False):
        st.header("Step 2")
        color = st.selectbox(label="Choose your favorite color",
                             options=["Azure", "Blue", "Cobalt", "Cyan", "Indigo", "Lapis lazuli"])
        if st.form_submit_button("Confirm choice"):
            st.session_state["step"] = 3
            st.session_state["color"] = color

def get_result(animal: str, color: str):
    return f"Congratulations! You chose the {color.lower()} {animal.lower()}"

def main():
    if st.session_state["step"]==1:
    elif st.session_state["step"]==2:
        result = get_result(animal=st.session_state["animal"], color=st.session_state["color"])


I could make some guesses about the timing of the form submission button getting pressed, the session state getting updated & the app rerunning, but it would take some investigation to know for sure.

However, in this case there’s an easy workaround, which is to do st.experimental_rerun() after updating the step number. This works well.

You mention that the first issue has been known for a while – do you know if this has been documented as an issue in the gitub repo yet? I searched a bit and didn’t find this specific issue. If it’s not currently there, would you mind adding a bug report at Issues · streamlit/streamlit · GitHub? Unfortunately, I don’t have a good idea for a fix, but am glad your workaround is working – I agree it definitely appears to be a bug.

Thank you @blackary; I was not aware of st.experimental_rerun() but it works well enough in this scenario.

Regarding the st.session_state issue, I feel like it used to come up a lot when multipage apps were implemented via selectboxes or radio buttons… so there should be some older issues which maybe got lost in the backlog?

IMO it’s a very confusing behaviour when you first come across it but I assumed everyone was working around it. Most of my streamlit code is littered with on_change callbacks which repopulate the “real” st.session_state from the “widget” session_state like so:

def _on_change_input():
    st.session_state["input"] = st.session_state["input"]

input = st.text_input("Please write something", key="input", on_change=_on_change_input)
1 Like

@ennui Thanks for pointing to those. Feel free to :+1: either or both of those, and to add another example of how this is problematic if you would like to. There are a lot of open issues, but the Streamlit team definitely pays attention to the bug reports and feature requests people make on there, especially if they get a lot of comments and reactions from people in the community.