Using `form_submit_button` with callback does not capture arg from other form input

Here’s a minimal non-working example:

import streamlit as st

def _handle_submission(user_input):
    st.write('You typed:')
    st.write(user_input)
    st.stop()

with st.form(key='form'):
    user_input = st.text_input(label='type here', key='user_input')
    st.form_submit_button(
        'Submit',
        on_click=_handle_submission,
        args=(user_input,))

This works fine if changed like so:

import streamlit as st

def _handle_submission(user_input):
    st.write('You typed:')
    st.write(user_input)
    st.stop()

with st.form(key='form'):
    user_input = st.text_input(label='type here', key='user_input')
    submit = st.form_submit_button('Submit')
    if submit:
        _handle_submission(user_input)

Tested both locally and deployed. Both behaved similarly.
Using Streamlit 1.33.0 and Python 3.11.

That is expected The new value will be assigned in the next rerun, but the callback is called before that so it is gets the old value. You can use session_state.user_input, though.

I like your working version, but I would put the conditional out of the with block. As the saying goes, flat is better than nested.

Thanks for confirming that this is WAI. I’m still puzzled why session_state can capture user_input correctly but not the argument passed to the callback. While using session_state for this might work as a workaround, it feels a bit hacky, and I’d prefer to avoid it if possible.

Because session_state.user_input is updated as soon as you press the submit button, but the assignment to user_input doesn’t happen until the next rerun.

Using session state in that way doesn’t feel hacky to me, but relying on global state can certainly become a problem in complex applications. There are ways to deal with that, but they are out of scope here IMO.

Anyway I would favour your second implementation, that delays the handling until the next rerun, when user_input has already been assigned the new value.

1 Like

I find this behavior quite counterintuitive. It’s natural to expect that the callback function would process the form input directly. If that’s not the case, the callback function loses much of its usefulness. At the very least, this should be clearly documented in my opinion.

I actually agree that it would be much more useful if the callback on st.form_submit_button would get passed all of the entries of the form automatically. I checked the issues, and I think this Directly provide form submission data to form submit callback · Issue #5498 · streamlit/streamlit · GitHub is the most relevant one – feel free to give this a :+1: to help it be prioritized and give your example use-case

Thanks, @blackary. I’ve thumbed up the issue.