I’m running locally using streamlit v1.40.2 and python 3.12.7. I’ve set up an example to illustrate a problem I’m seeing. I have a data_editor with checkboxes and I also have a button that I want to use to ‘shuffle’ the ordering of the dataframe. The shuffling works, but if I “Shuffle” and then attempt to click multiple checkboxes I get strange behavior. Clicking on the checkbox seems to work fine, but the second one loses its state. Similar happens for further checkboxes.
This is a simple example that illustrates the issue:
Screenshot:
Code:
import streamlit as st
import pandas as pd
# Sample data
df = pd.DataFrame({
'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
'Age': [25, 30, 35, 40, 45],
'Score': [80, 90, 70, 60, 50]
})
# Initialize session state for the shuffled DataFrame and checkboxes
if 'shuffled_df' not in st.session_state:
st.session_state.shuffled_df = df.copy()
if 'checkbox_states' not in st.session_state:
st.session_state.checkbox_states = [False] * len(df)
# Function to shuffle the DataFrame
def shuffle_dataframe():
st.session_state.shuffled_df = st.session_state.shuffled_df.sample(frac=1).reset_index(drop=True)
# Reset checkbox states to match the shuffled DataFrame
st.session_state.checkbox_states = [False] * len(st.session_state.shuffled_df)
# Dropdown to filter by Age
selected_age = st.selectbox(
"Select Minimum Age",
options=sorted(df['Age'].unique())
)
# Filter the shuffled DataFrame based on Age
filtered_df = st.session_state.shuffled_df[st.session_state.shuffled_df['Age'] >= selected_age]
# Map global checkbox states to the filtered DataFrame
filtered_indices = filtered_df.index.tolist()
filtered_df['Selected'] = [st.session_state.checkbox_states[i] for i in filtered_indices]
# Button to shuffle the DataFrame
st.button("Shuffle DataFrame", on_click=shuffle_dataframe)
# Use st.data_editor to edit the filtered DataFrame with checkboxes
edited_df = st.data_editor(filtered_df, use_container_width=True)
# Update global checkbox states based on edits
for idx, selected in zip(filtered_indices, edited_df['Selected']):
st.session_state.checkbox_states[idx] = selected
# Debugging output
st.write("Shuffled DataFrame:")
st.write(st.session_state.shuffled_df)
st.write("Global Checkbox States:")
st.write(st.session_state.checkbox_states)
I’ve tried to debug with the help of ChatGPT, but I am going in circles. Can anyone lend a hand?
This “double submit” problem is a common source of confusion. Widgets lose state when you modify their definition. In this case, you are redefining filtered_df
and the widget is getting reinitialized. The logic is as follow:
Script run:
filtered_df
is all false in the “Selected” column.
- The data editor has nothing checked.
st.session_state.checkbox_states
is all false.
- The script run completes, and then you check a box in the first row (trigger a rerun).
Script run:
filtered_df
is (still) all false in the “Selected” column.
- The data editor shows the first row checked (the change is saved in the state of the widget).
st.session_state.checkbox_states
is updated from the output of the data editor and has one true value.
- The script run completes, and then you check a box in the second row (trigger a rerun).
Script run:
filtered_df
has one true value obtained from st.session_state.checkbox_states
.
- Streamlit reinitializes the widget because it sees a different dataframe being passed to it than it saw in the previous script run. Therefore, the state (including the second check) is discarded and it renders with only the first checkbox.
The trick is to make these kind of changes in a callback (instead of using the widget’s output).
For example, you could change your code like this (only handling the checkbox columns):
import streamlit as st
import pandas as pd
# Sample data
df = pd.DataFrame({
'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
'Age': [25, 30, 35, 40, 45],
'Score': [80, 90, 70, 60, 50]
})
# Initialize session state for the shuffled DataFrame and checkboxes
if 'shuffled_df' not in st.session_state:
st.session_state.shuffled_df = df.copy()
if 'checkbox_states' not in st.session_state:
st.session_state.checkbox_states = [False] * len(df)
# Function to shuffle the DataFrame
def shuffle_dataframe():
st.session_state.shuffled_df = st.session_state.shuffled_df.sample(frac=1).reset_index(drop=True)
# Reset checkbox states to match the shuffled DataFrame
st.session_state.checkbox_states = [False] * len(st.session_state.shuffled_df)
def update_checks():
for key, edits in st.session_state.editor["edited_rows"].items():
for col, value in edits.items():
if col=="Selected":
st.session_state.checkbox_states[key] = value
# Dropdown to filter by Age
selected_age = st.selectbox(
"Select Minimum Age",
options=sorted(df['Age'].unique())
)
# Filter the shuffled DataFrame based on Age
filtered_df = st.session_state.shuffled_df[st.session_state.shuffled_df['Age'] >= selected_age]
# Map global checkbox states to the filtered DataFrame
filtered_indices = filtered_df.index.tolist()
filtered_df['Selected'] = [st.session_state.checkbox_states[i] for i in filtered_indices]
# Button to shuffle the DataFrame
st.button("Shuffle DataFrame", on_click=shuffle_dataframe)
# Use st.data_editor to edit the filtered DataFrame with checkboxes
edited_df = st.data_editor(filtered_df, use_container_width=True, key="editor", on_change=update_checks)
# Debugging output
st.write("Shuffled DataFrame:")
st.write(st.session_state.shuffled_df)
st.write("Global Checkbox States:")
st.write(st.session_state.checkbox_states)
1 Like