skip to content

In Streamlit, how do you stop every widget change from rerunning the whole script?

level: middleimportance: nice to knowfreq 35%

answer

  1. the default is a full script run
  2. several inputs often belong together
  3. one press instead of one per keystroke
  4. a decorator can scope the rerun narrowly
  5. making the rerun cheap also counts

basics

~20 s

Wrap related inputs in st.form so nothing reruns until the submit button is pressed, scope an interactive section with a fragment so only that function reruns, cache expensive functions, and gate heavy work behind a stored decision.

solid answer

~50 s

Three complementary tools. `st.form` batches widgets: everything inside it is collected locally and no rerun happens until `st.form_submit_button` is clicked, which is the right answer for a filter panel with five inputs where each keystroke would otherwise fire a query. A fragment — a function decorated with `@st.fragment` — reruns only itself when a widget inside it changes, leaving the rest of the page untouched, which suits a small interactive corner of an otherwise expensive page. Caching with `st.cache_data` does not stop the rerun but makes it nearly free, and is usually the first thing to reach for. Finally, gate genuinely heavy work behind a flag in `st.session_state` set by a button, so it runs on demand rather than on every interaction. Note that widgets inside a form cannot use `on_change` callbacks and only `st.form_submit_button` is allowed as a button there.

code

python · 9 lines
python
import streamlit as st

with st.form("filters"):
    region = st.selectbox("region", ["emea", "amer", "apac"])
    minimum = st.number_input("min value", 0)
    submitted = st.form_submit_button("apply")   # the only rerun trigger

if submitted:
    st.dataframe(run_query(region, minimum))

go deeper

for a junior

Recall that st.form collects several inputs and reruns only when the submit button is pressed, and that caching makes repeated work cheap. That covers the common case.

for a middle

Explain the difference between making a rerun cheap and stopping it: caching removes the cost, forms batch the trigger, fragments narrow the scope. Know the form's restrictions on buttons and callbacks.

for a senior

Show you choose deliberately based on what the rerun costs and whether the work has side effects, and that you push long-running work off the interaction path rather than optimising the rerun around it.

for a principal

Own the interaction budget for internal apps: what latency users will tolerate, what a per-keystroke query costs the warehouse, and when an app that needs this much rerun engineering should have been a BI dashboard or a service instead.

## The problem Streamlit re-executes the whole script on every widget interaction. For a page whose body is a couple of charts over a cached DataFrame that is fine — a rerun costs milliseconds. It stops being fine when the script issues a warehouse query, calls a paid API, or trains something, and the user is typing in a text box. Each keystroke is an interaction, so each keystroke is a full script run. There are four levers, and they are not alternatives so much as layers. ## 1. Make the rerun cheap: caching Before trying to suppress reruns, remove their cost. Wrapping the query in `st.cache_data` means the rerun still happens but the expensive call does not. This is the default answer and solves most cases; the other levers exist for the work that genuinely cannot be cached, such as a mutation, a paid call with side effects, or a computation whose inputs change every time. ## 2. Batch the inputs: st.form ```python with st.form("filters"): region = st.selectbox("region", REGIONS) start = st.date_input("from") minimum = st.number_input("min value", 0) submitted = st.form_submit_button("apply") if submitted: run_query(region, start, minimum) ``` Widgets inside a form do not send their changes to the server as they are edited. The user fills in all three, presses **apply**, and exactly one rerun happens, with all three values updated together. That also fixes a subtler problem: without a form, a user editing three filters causes three reruns, two of which query a half-specified state. Constraints worth remembering: the only button allowed inside a form is `st.form_submit_button`, and widgets inside a form cannot take `on_change` / `on_click` callbacks (the submit button itself can take `on_click`). A form needs a unique key. And a form does not make the *submit* cheap — it makes the intermediate states free. ## 3. Narrow the rerun: fragments A fragment is a function decorated with `@st.fragment`. Widgets inside it trigger a rerun of that function alone; the rest of the script does not re-execute. ```python @st.fragment def detail_panel(df): row = st.selectbox("row", df.index) st.write(df.loc[row]) data = load_expensive() # not re-run when the selectbox changes detail_panel(data) ``` This is the tool for an expensive page with one chatty interactive corner. The mental model shifts slightly — the fragment reruns in isolation, so it sees session state but does not rebuild the surrounding page — and you can still force a full rerun from inside it when you need the page to catch up. Fragments arrived as `st.experimental_fragment` and were later promoted to `st.fragment`, so check what your pinned version exposes; a fragment can also be given a periodic refresh interval, which is the idiomatic way to build a live-updating tile without polling the whole script. ## 4. Gate the work: buttons plus session state Some work should simply not happen unless asked for: ```python if st.button("run backfill"): st.session_state["backfill_requested"] = True if st.session_state.get("backfill_requested"): do_backfill() ``` Remember that `st.button` is `True` only on the run its click triggered, which is why the decision is recorded in session state rather than acted on inline. ## Choosing between them Ask what the rerun costs and what the user is doing. Expensive but pure and repeatable? Cache it. Several inputs that only make sense together? Form. One noisy widget on an otherwise heavy page? Fragment. Something with side effects or a long runtime? Button plus session state, and ideally push the work to a queue so a thread is not held. In a real app you will use three of the four at once, and an interviewer asking this question is usually checking that you know a rerun is the default and that suppressing it is a deliberate design act.

  • Why can't a widget inside an st.form use an on_change callback?
    Because form widgets deliberately do not communicate with the server until submit, and a callback exists to run at interaction time. There is nothing to fire. Put the callback on st.form_submit_button, or take the widget out of the form if it genuinely needs to react as the user types.
  • When is caching a better answer than a form?
    Whenever the expensive step is a pure function of its inputs. Caching keeps the app fully reactive — every keystroke still updates the view — while costing nothing on repeat values. A form is for when the intermediate states are meaningless or the work has side effects, not merely slow.
  • What is a fragment not able to fix?
    A slow first render. A fragment narrows subsequent reruns; it does not make the initial full script run any faster, and it does not help when the expensive work is upstream of the interaction. It also does not change the fact that heavy work occupies a thread in a process shared with other users.

saying these in an interview costs you the question

  • Thinks a form makes the submitted query itself faster
  • Puts a plain st.button inside a form and expects it to work
  • Reaches for fragments before caching the expensive call
  • Assumes a fragment rerun refreshes the whole page too
  • Believes suppressing reruns removes the need for session state

context