Forms: batching input

Streamlit · Practical · 10 min

Forms stop expensive reruns while users fill several fields, and match how people expect to enter data.

Normally each widget change triggers a rerun. Inside `st.form`, widget changes are held in the browser until the user clicks the form's submit button; then the script reruns once with all the new values.

with st.form("signup"):
    email = st.text_input("Email")
    plan = st.radio("Plan", ["free", "pro"])
    submitted = st.form_submit_button("Sign up")
if submitted:
    save(email, plan)

Rules:

  • Every form needs at least one st.form_submit_button; Streamlit shows an error otherwise.
  • Ordinary st.button and st.download_button are not allowed inside forms.
  • Only the submit button can have a callback inside a form; widgets inside cannot use on_change. Use on_click on the submit button, which runs before the rerun with all form values available in session state (via keys).
  • clear_on_submit=True resets the inputs after a submission, useful for entry forms.
  • enter_to_submit controls whether pressing Enter in a text field submits.

Before submission, widget return values are their previous (or default) values, so read values in the if submitted: branch.

Forms are ideal when inputs only make sense together (a search with several filters, a record to save), or when each rerun is expensive. For instant feedback on every change, use plain widgets.

Example

import streamlit as st

if "orders" not in st.session_state:
    st.session_state.orders = []

def place_order():                     # submit callback: values are in state
    s = st.session_state
    s.orders.append({"item": s.item, "qty": s.qty, "rush": s.rush})

with st.form("order", clear_on_submit=True):
    st.selectbox("Item", ["Tea", "Coffee", "Cocoa"], key="item")
    st.number_input("Quantity", min_value=1, max_value=20, value=1, key="qty")
    st.checkbox("Rush delivery", key="rush")
    submitted = st.form_submit_button("Place order", on_click=place_order)

if submitted:
    st.success(f"Order #{len(st.session_state.orders)} placed")
st.write(st.session_state.orders)
# Changing the inputs does not rerun the app; only clicking the button does.

Remember

  • Widgets inside st.form only trigger a rerun when the form is submitted
  • Each form needs a st.form_submit_button; only it may have a callback
  • Read values after submission, or in the submit callback via keys

Watch out for

  • Adding on_change callbacks to widgets inside a form, which Streamlit does not allow.

Check yourself

  1. A form has five inputs. How many reruns happen when a user edits all five and clicks submit once?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.streamlit.io/develop/api-reference/execution-flow/st.form