St.form and dynamic st.selectbox dilemma

I have a simple reproducible example (the full code will be at the bottom) to illustrate the problem.

First I have 4 dataframes that are stored in a list. The dataframes are simplified versions, so there is only 1 column called col. In each dataframe, the first entry is the condition, which in this example the condition is either A, B or C. The purpose of the app is to handle each dataframe one by one while extracting information based on the condition.

# 4 dataframes that are stored in a list
data1 = {
    "col": ["A", "random1", "random2", "S1", "random3", "P123"]
}
data2 = {
    "col": ["A", "random3", "random4", "S1", "random10", "P124"]
}
data3 = {
    "col": ["B", "random99", "S2", "P125"]
}
data4 = {
    "col": ["C", "g4rr", "dsf31", "dsfjjb", "sdfdfsd", "123jbds", "01bdwe"]
}
df1 = pd.DataFrame(data1)
df2 = pd.DataFrame(data2)
df3 = pd.DataFrame(data3)
df4 = pd.DataFrame(data4)
list1 = [df1, df2, df3, df4]

For example, if the condition is A, the supplier_name_value can be extracted in the [3] index of the column.
If the condition is B, the supplier_name_value can be extracted in the [2] index.
If the condition is C, supplier_name_value is empty.

# supplier name value and po number value differs depending on the condition
if list1[st.session_state.page]['col'][0] == "A":
    supplier_name_value = list1[st.session_state.page]['col'][3]
    po_number_value = list1[st.session_state.page]['col'][5]
elif list1[st.session_state.page]['col'][0] == "B":
    supplier_name_value = list1[st.session_state.page]['col'][2]
    po_number_value = list1[st.session_state.page]['col'][3]
else:
    supplier_name_value = ""
    po_number_value = ""

If the supplier_name_value is not empty, then the dataframe is relevant, and selectbox is chosen as Yes. Otherwise, it’s chosen as No.

if supplier_name_value:
    choice = st.selectbox(label="Relevant?", options=["Yes", "No"], index=0)
else:
    choice = st.selectbox(label="Relevant?", options=["Yes", "No"], index=1)

If the selectbox is Yes, the supplier_name_value and po_number_value will be extracted into text_input which are in a form.

Else, no text_input

When the user clicks Next, it saves those information to a spreadsheet (not included in the code) and then goes to the next dataframe in the list, and relevant information will be extracted. It works great when user don’t need to change the information, but there is problem when the user needs to change things.

Problem

  1. For example, on the first dataframe where the selectbox is Yes because the condition is A. If the user overrides the selectbox to No, the text_input will disappear (as expected) and user can click Next. But for the second dataframe where the selectbox should be Yes again, it somehow won’t update the selectbox, and it stays as No.
  2. The reason to use st.form is because it has an option to clear_on_submit, so everytime I go to the next dataframe, those fields will be refreshed which allows user to override the original fields without worrying about the fields not updating for the next dataframe.
  3. I can’t put selectbox in the st.form, because it won’t allow user override to dynamically change the form, because the form waits for user input first before the submit button is clicked.

Expected Behaviour

For example, what’s displayed below is the first dataframe. Because it has condition A, thus the supplier_name and po_number is pulled using their relevant positions and the information is extracted to the st.form. The user checks if the pulled information is correct, then click next. The user is not trying to correct the dataframe, but simply checking if the extracted info is correct, if it’s correct, the user clicks next, if it’s not right, the user changes the text inside the form, then click next. The information will be saved elsewhere (this saving process is not included in the demo app), but just think about everytime the user clicks next, the info is saved to a spreadsheet.

After the user clicks next, it goes to the next dataframe in the list, the user checks again, then clicks next. Then follow the same procedure over and over.

Now back to the first dataframe, when the user examines the pulled information and the dataframe. The user realised the algorithm made a mistake, and this dataframe should not be relevant, and thus don’t require the information to be extracted. So the user selects No in the selectbox, then clicks next.


What I’m hoping to see is it goes to the next dataframe and the below is presented.

But what actually happened is, the Relevant selectbox is still No because it didn’t get refreshed.


My assumption is that streamlit sees because the condition is the same with dataframe2 and dataframe1, both have condition A. So when it advances to dataframe2, it decides not to run the script from top to bottom again.

Summary: what I want the app to do is, after the user clicks next, it goes to the next dataframe, while running the python script from top to bottom again (for example, to reassess if the dataframe is relevant or not

if supplier_name_value:
    choice = st.selectbox(label="Relevant?", options=["Yes", "No"], index=0)
else:
    choice = st.selectbox(label="Relevant?", options=["Yes", "No"], index=1)

Full Code

import streamlit as st
import pandas as pd

if 'page' not in st.session_state:
    st.session_state.page = 0
# 4 dataframes that are stored in a list
data1 = {
    "col": ["A", "random1", "random2", "S1", "random3", "P123"]
}
data2 = {
    "col": ["A", "random3", "random4", "S1", "random10", "P124"]
}
data3 = {
    "col": ["B", "random99", "S2", "P125"]
}
data4 = {
    "col": ["C", "g4rr", "dsf31", "dsfjjb", "sdfdfsd", "123jbds", "01bdwe"]
}
df1 = pd.DataFrame(data1)
df2 = pd.DataFrame(data2)
df3 = pd.DataFrame(data3)
df4 = pd.DataFrame(data4)
list1 = [df1, df2, df3, df4]

# display the list
st.write(list1[st.session_state.page])

# supplier name value and po number value differs depending on the condition
if list1[st.session_state.page]['col'][0] == "A":
    supplier_name_value = list1[st.session_state.page]['col'][3]
    po_number_value = list1[st.session_state.page]['col'][5]
elif list1[st.session_state.page]['col'][0] == "B":
    supplier_name_value = list1[st.session_state.page]['col'][2]
    po_number_value = list1[st.session_state.page]['col'][3]
else:
    supplier_name_value = ""
    po_number_value = ""

# if the supplier name value is not empty, then the selectbox is Yes, otherwise No
if supplier_name_value:
    choice = st.selectbox(label="Relevant?", options=["Yes", "No"], index=0)
else:
    choice = st.selectbox(label="Relevant?", options=["Yes", "No"], index=1)

# if the selectbox is yes, then there are fields to be populated, otherwise No
if choice == "Yes":
    with st.form(key="my_form", clear_on_submit=True):
        supplier_name = st.text_input(label="Supplier Name", value=supplier_name_value)
        po_number = st.text_input(label="PO Number", value=po_number_value)
        next_button = st.form_submit_button(label="Next")
        if next_button:
            if st.session_state.page + 1 < len(list1):
                st.session_state.page += 1
                st.experimental_rerun()
            else:
                st.markdown("It's already the last one")
else:
    next_button2 = st.button(label="Next")
    if next_button2:
        if st.session_state.page + 1 < len(list1):
            st.session_state.page += 1
            st.experimental_rerun()
        else:
            st.markdown("It's already the last one")

I’ve read through your example a few times, and run your example code, and it’s still not entirely clear to me what the expected behavior is, and what it’s doing that is different.

Could you be more explicit in the expected behavior section?

e.g. “If the user chooses yes, changes the values and then hits submit, then the form would show xyz”

This would make it much easier to test and debug what’s going on.

Also, both for people trying to answer on the forum, and for your own debugging, I would try removing more and more in your simplified version of the code until you really have it down to a minimal reproducible example.

Things I would remove:

  1. You’re not using any special properties of dataframes, so just make them dictionaries
  2. Simplify your A, B, and “else” ifs to just be two options, “A” and “else”
  3. Shorten your list down to two examples, unless 3 is required to see the issue
  4. Rather than having supplier name value and po number value, just simplify your data to be dictionaries with only two values, or better yet just tuples:
  • A, B, or C
  • A value

e.g. combining a few of those, your list1 could become

list1 = [("A", "something"), ("B", "something_else")]

The goal of all this being, there are a lot of things going on even in your simplified example, and it’s possible that either there’s 1. a logic bug, or that 2. you’re trying to use streamlit in a way that won’t work, or 3. there’s a bug in streamlit, but all the extra stuff and the high-level version of the expected behavior, rather than a detailed step-by-step makes it harder to sort out what’s going on.

That being said, thank you so much for giving a detailed explanation, and fully-functional example code – that does help a lot! It’s just still not quite enough for me to grasp what’s really the issue here.

3 Likes

Thanks, I have added more in the Expected Behaviour section, but it’s a bit long, as I’m a bit struggling to find an easy way to explain it, apology english isn’t my first language, but will improve it to make it more easy to understand. The other thing I want to explain a bit about the background so that you understand why it’s dataframes in a list, not dict.

This is an app to read pdf one by one, the user checks if the info on pdf and the info pulled to the st.form matches. The app works by transforming pdf to text strings then store those text strings in a dataframe. Sometimes there are scanned copies, so OCR will be used to do the transformation.

If your Next button is staying on the same page in Streamlit, then any widget that doesn’t have its creation parameters changed will still think of itself as the same widget (and thus not go back to its default value if you go to the next pdf page).

Take your example of changing Yes → No on the first df of type A, then clicking next. Now you are looking at the second df also of type A. However the selectbox is the same. Streamlit doesn’t understand that you want to initialize a new selectbox that happens to have the same starting parameters. It thinks this is in fact the same selectbox and it is remembering that the user changed it.

To fix this, I recommend adding keys to all your widgets that include some unique identifier per page to let Streamlit know you want a brand new widget. And just a step further for clarity:

if supplier_name_value:
    default_index = 0
else:
    default_index = 1

choice = st.selectbox(label="Relevant?", options=["Yes", "No"], index=default_index, key = f'relevance_widget_{st.session_state.page}')

Do the same for your supplier and PO widgets. Something like
key=f'supplier_widget_{st.session_state.page}'
and
key=f'PO_widget_{st.session_state.page}'

1 Like

Never thought about initiating new widgets for different dataframes, thanks! Would there be any performance issues by doing things this way? This should work, but will test it later and confirm if it’s the solution, thanks again!

Full Solution

Thanks for the tips from @mathcatsand which I have marked as the solution. But for anyone who’s interested in a solution with full code, please see below. The trick is to initiate widgets using different key to “force” the widgets to refresh.

import streamlit as st
import pandas as pd

if 'page' not in st.session_state:
    st.session_state.page = 0
# 4 dataframes that are stored in a list
data1 = {
    "col": ["A", "random1", "random2", "S1", "random3", "P123"]
}
data2 = {
    "col": ["A", "random3", "random4", "S1", "random10", "P124"]
}
data3 = {
    "col": ["B", "random99", "S2", "P125"]
}
data4 = {
    "col": ["C", "g4rr", "dsf31", "dsfjjb", "sdfdfsd", "123jbds", "01bdwe"]
}
df1 = pd.DataFrame(data1)
df2 = pd.DataFrame(data2)
df3 = pd.DataFrame(data3)
df4 = pd.DataFrame(data4)
list1 = [df1, df2, df3, df4]

# display the list
st.write(list1[st.session_state.page])

# supplier name value and po number value differs depending on the condition
if list1[st.session_state.page]['col'][0] == "A":
    supplier_name_value = list1[st.session_state.page]['col'][3]
    po_number_value = list1[st.session_state.page]['col'][5]
elif list1[st.session_state.page]['col'][0] == "B":
    supplier_name_value = list1[st.session_state.page]['col'][2]
    po_number_value = list1[st.session_state.page]['col'][3]
else:
    supplier_name_value = ""
    po_number_value = ""

# if the supplier name value is not empty, then the selectbox is Yes, otherwise No
if supplier_name_value:
    default_index = 0
else:
    default_index = 1

choice = st.selectbox(label="Relevant?", options=["Yes", "No"], index=default_index,
                      key=f"relevance_widget_{st.session_state.page+1}")

# if the selectbox is yes, then there are fields to be populated, otherwise No
if choice == "Yes":
    with st.form(key="my_form", clear_on_submit=True):
        supplier_name = st.text_input(label="Supplier Name", value=supplier_name_value,
                                      key=f"supplier_name_{st.session_state.page+1}")
        po_number = st.text_input(label="PO Number", value=po_number_value,
                                  key=f"po_number_{st.session_state.page+1}")
        next_button = st.form_submit_button(label="Next")
        if next_button:
            if st.session_state.page + 1 < len(list1):
                st.session_state.page += 1
                st.experimental_rerun()
            else:
                st.markdown("It's already the last one")
else:
    next_button2 = st.button(label="Next")
    if next_button2:
        if st.session_state.page + 1 < len(list1):
            st.session_state.page += 1
            st.experimental_rerun()
        else:
            st.markdown("It's already the last one")
1 Like

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.