Need help: st.data_editor saves changes intermittently

I’m using st.data_editor, which includes a checkbox column called “Save.” The expected flow is:

  1. The user edits one or more rows.
  2. Then they select the “Save” checkbox for the edited rows.
  3. Finally, they press the “Save Changes” button to process and save those records.

However, the behavior is inconsistent:

  1. Sometimes, when pressing the button, the message appears:
    “No records selected for saving,” even when I had selected checkboxes.
  2. Other times, the checkboxes are unchecked by themselves and the changes are not reflected.
  3. But sometimes it works correctly and saves the changes as expected.

I think it’s due to the loss of the checkbox state or the edits.

I would like

  1. The edits made to be retained.
  2. The selected checkbox to remain selected until the save is processed.
  3. The “Save Changes” button works reliably and consistently.
    Is this possible?

This is the table and the save button.


   with st.form("form_guardado"):
        # tabla editable con columnas personalizadas, especifica como debe verse cada columna, ejm "Guardar": como checkbox, "Incidente": texto con límite de 7 caracteres,hide_index=True sirve para eliminar el index etc
        df_edited  = st.data_editor(df_styled, key="unique_data_editor", column_config={'id': None, 'Guardar': st.column_config.CheckboxColumn("Guardar", default=False), 'Incidente': st.column_config.TextColumn("Incidente", max_chars=7), 'Solucion_Ejecutada': st.column_config.TextColumn("Solución Ejecutada"),'Causa_Raiz': st.column_config.TextColumn("Causa_Raiz"),'Solucion2': st.column_config.TextColumn("Solucion2"),'Resumen_OT': st.column_config.TextColumn("Resumen_OT"),'Plantilla_OT': st.column_config.TextColumn("Plantilla_OT"), 'Avance' : st.column_config.TextColumn("Avance"),'HORA_INICIO': st.column_config.DatetimeColumn("HORA_INICIO", format="YYYY-MM-DD HH:mm:00"),'HORA_RECUPERACION': st.column_config.DatetimeColumn("HORA_RECUPERACION", format="YYYY-MM-DD HH:mm:00"),'Version': st.column_config.NumberColumn("Versión", disabled=True)}, disabled=["id","TMR","Tiempo_Efectivo", "Solo_CONTRATISTAS","Tiempo_Decimal","Tiempo_Efectivo_Decimal","Solucion_Ejecutada","Causa_Raiz","Solucion2","Resumen_OT", "Avance", "Estado_avance", "Usuario"], hide_index=True)
        # Se ejecuta solo cuando el usuario presiona el botón
        if st.form_submit_button("💾 Guardar cambios"):
            # Muestra qué registros están marcados con el checkbox "Guardar"
            try:
                st.write("Valores de 'Guardar':", df_edited['Guardar'].unique())
                f_save = df_edited[df_edited['Guardar']]
                st.write("Registros a guardar:", f_save)
                # Si ningún registro fue marcado, se detiene la ejecución con advertencia
                if f_save.empty:
                    st.warning("No hay registros seleccionados para guardar")
                    st.stop()
                # Conversión de fechas y tiempos
                f_save['HORA_INICIO'] = pd.to_datetime(f_save['HORA_INICIO'], errors='coerce')
                f_save['HORA_RECUPERACION'] = pd.to_datetime(f_save['HORA_RECUPERACION'], errors='coerce')
                for col in ['Tiempo_Reloj', 'Tiempo_Telefonica']:
                    f_save[col] = pd.to_timedelta(f_save[col], errors='coerce')
                f_save['TMR'] = f_save.apply(operacion_tmr, axis=1)
                f_save['Solo_CONTRATISTAS'] = f_save.apply(operacion_contratista, axis=1)
                f_save['Tiempo_Efectivo'] = f_save.apply(operacion_tefectivo, axis=1)
                f_save['Tiempo_Decimal'] = f_save['Tiempo_Reloj'].apply(operacion_tdecimal)
                f_save['Tiempo_Efectivo_Decimal'] = f_save.apply(operacion_ted, axis=1)

                # Guardado
                st.toast(f"Guardando {len(f_save)} registros...", icon="💾")
                usuario = st.session_state.get("Usuario", "Desconocido")

                progress_bar = st.progress(0)
                status_text = st.empty()
                # llama a la funcion guardar_registros, df_edited: la tabla completa, f_save: solo los registros a guardar, usuario: usuario activo en sesión
                exito = guardar_registros(df_edited, f_save, usuario)
                # si fue exitoso recarga los datos desde la base consulta_mysql()
                if exito:
                    progress_bar.progress(100)
                    status_text.success("Datos actualizados")
                    st.session_state.df = consulta_mysql() # función que obtiene los datos desde la bd
                    st.session_state.df['Guardar'] = False
                    st.rerun()
                else:
                    progress_bar.empty()
                    status_text.error("Error al guardar, verifique los datos")
            # si ocurre un error muestra mensaje
            except Exception as e:
                st.error(f"Error crítico al guardar: {str(e)}")
                st.exception(e)

This is the editable part of the table with the button.


Thank you!

This might help you

  1. Add a dedicated session state key for the data editor to persist checkbox states between reruns:
    if ‘editor_state’ not in st.session_state:
    st.session_state.editor_state = None

with st.form(“form_guardado”):
df_edited = st.data_editor(
df_styled,
key=“editor_key”, # Unique key for widget state
column_config={…}, # Your existing config
on_change=lambda: st.session_state.update(editor_state=st.session_state.editor_key)
)
2. Modify your submit handler to use the persisted state:
if st.form_submit_button(“:floppy_disk: Guardar cambios”):
# Use session state instead of df_edited directly
if st.session_state.editor_state is not None:
f_save = st.session_state.editor_state[st.session_state.editor_state[‘Guardar’]]

if f_save.empty:
    st.warning("No hay registros seleccionados para guardar")
else:
    # Your existing save logic here
    ...
  1. After successful save, explicitly reset checkboxes:
    if exito:

    st.session_state.df[‘Guardar’] = False # Reset checkboxes
    if ‘editor_key’ in st.session_state:
    del st.session_state.editor_key # Clear widget state
    st.rerun()

Thanks for your reply, but the StreamlitAPIException is thrown: With forms, callbacks can only be defined on the st.form_submit_button button. Defining callbacks on other widgets within a form is not allowed.
I delete the form and StreamlitAPIException is no longer the problem, now it’s this one.

KeyError: 'Guardar'
Traceback:
in main
    f_save = st.session_state.editor_state[st.session_state.editor_state['Guardar']]
                                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^

But if possible I would like it to be on the form since it loads much faster.
Thank you