Button callback function is unexpectedly executed upon page load

Iโ€™m trying to implement callback functions instead of clumsy workarounds like:

is_clicked = st.button()

if is_clicked:
    [code here]

So I have a button with a callback function that alters session state keys:

def callback_func(arg):
    st.session_state[arg] = arg_stuff

    # Clear the session state Key1 for something not arg-related
    st.session_state["Key1"] = []   


def main():
    df = pd.read_sql_query(sql,conn)
    result = AgGrid(df,grid_options(df))    # Using AgGrid for single row select
    st.button("Save to session state", on_click=callback_func(result))

Two things:

  1. Clearing the session state key generates a StreamlitAPIException as follows:
StreamlitAPIException: st.session_state.Key1 cannot be modified after the widget with key Key1 is instantiated.
  1. This error is generated when I make a selection in the AgGrid table, not when I click the button. In fact, the button doesnโ€™t even render.

  2. When I rerun the app leaving the AgGrid row selected, the screen refreshes with the same row selection and the same exception.

Major question: why is my callback function being executed before the button is being clicked? Even if I remove the exception-causing code, I can see the session state arg key being populated after AgGrid row selection without having me click the button first (to trigger the callback function).

(If someone wants to opine on the session state stuff, thatโ€™d be nice too, but maybe thatโ€™s for another thread. I want to clear an input widget based on this AgGrid table selection.)

I think part of this is due to my erroneous use of the callback function:

# WRONG
st.button("Foo",on_click=callback_func(result))

# RIGHT
st.button("Foo",on_click=callback_func,args=result)

args takes a tuple, and kwargs takes a dict. I have a list, ugh.

Anyway, maybe my erroneous declaration of that callback function is being interpreted as something to execute??

Yes, you have correctly identified what I imagine is the bulk of the problem. on_click needs to receive a callable. If you put on_click=callback_func(result) then I believe callback(result) is going to execute when the associated widget it mounted (during the page load and before a user clicks the button). In general, args is computed when the widget is rendered, not when the widget is clicked. See the โ€œNoteโ€ in this section.

As for args needing a tuple, it can accept a list also. However, if you want to pass a single value, even if it happens to be a list itself, put it in square brackets. The contents of the list or tuple passed to args are used in the callback function. In your case, even though result is a list already, it is the list itself as a single object that you are trying to pass:

args = [result]

With these corrections, do you still have a problem?

All is well, thanks! I might suggest that Streamlit throw an exception or error if someone assigns a malformed callable (with arguments) to on_click. It seems like it could be a common mistake.

1 Like

What if itโ€™s a function returning a callable?

You may want to resort to my clumsy workaround, which still works.