In Streamlit, what happens to your Python script when a user moves a slider?
answer
- the script is the render function
- interaction means re-execute, not update
- widget calls return values, not events
- local variables are rebuilt every time
- only session state and caches persist
basics
~10 sStreamlit re-executes the whole script from top to bottom, and st.slider returns the new value on that run. Ordinary local variables are rebuilt from scratch; only session state and cached results survive between runs.
solid answer
~50 sStreamlit has no widget callbacks or event loop in the usual sense. Every interaction — dragging a slider, typing in a text box, clicking a button — sends the new widget values to the server over the app's websocket and re-executes the entire script as a fresh top-to-bottom run. Widget calls such as `st.slider(...)` simply return the current value for that run, which is why the code reads like a linear script even though the app is reactive. The consequences follow directly: plain Python variables are recreated every run, expensive work repeats unless wrapped in `st.cache_data` or `st.cache_resource`, and anything that must persist across interactions has to live in `st.session_state`. `st.button` is the classic trap — it returns `True` only on the single run triggered by the click and `False` on every rerun after that.
code
python · 8 linesimport streamlit as st
st.write("this line executes on every interaction")
n = st.slider("rows", 1, 100, 10) # returns the CURRENT value each run
total = 0 # re-initialised on every rerun
total += n
st.write(total) # never accumulates across dragsgo deeper
Be ready to say plainly that any interaction re-runs the whole script and that widget functions return their current value. Knowing that variables reset and st.session_state is the fix is most of what a screen asks for.
Explain the mechanics: same process, fresh script run, values arriving over the websocket, elements diffed onto the page. Be able to walk through why st.button is True for exactly one run.
Show you design around repeated execution — caching boundaries, forms to batch input, guards so a query does not fire per keystroke — and can debug an app that hammers the warehouse because nothing was cached.
Own the tradeoff: the rerun model trades runtime efficiency for authoring simplicity, which is right for internal tools and wrong for high-concurrency, low-latency products. Be able to say where that line sits for your organisation.
## The execution model in one sentence A Streamlit app *is* a Python script, and Streamlit's answer to "the user did something" is "run the script again". There is no retained widget tree you mutate, no `onChange` handler wiring, and no long-lived object graph you update in place. The script is the render function, and every interaction re-invokes it. ## What actually happens on an interaction When you open the app, the browser establishes a websocket to the Streamlit server, which runs your script once and streams the resulting elements to the page. When you move a slider, the browser sends the new widget value up that same connection. The server then runs your script again, from line one, inside the same Python process, with the updated widget values available. Elements produced by the new run replace the ones from the old run in place, so the page updates without a full reload. It is a rerun, not a restart: the process, imported modules, module-level globals created at import time and the caches are all still there. What is thrown away is everything the *previous run of your script body* computed and did not deliberately store. ## Widgets are function calls that return values ```python n = st.slider("rows", 1, 100, 10) df = load_data().head(n) st.dataframe(df) ``` `st.slider` renders a slider *and* returns its current value for this run. The first run returns the default (10); after the user drags it to 40, the script runs again and the same call returns 40. Reading the code top to bottom therefore describes the app at any point in time, which is the whole ergonomic payoff of the framework: you write a script, you get a reactive app. ## What survives a rerun, and what does not Survives: - **`st.session_state`** — a dict-like store scoped to one browser session, explicitly designed to carry values across reruns. - **Cached values** — anything returned through `st.cache_data` or `st.cache_resource`, which are keyed server-side and shared across sessions. - **The process itself**, including module imports and anything created at import time. Does not survive: - Ordinary variables assigned in the script body. `total = 0` at the top means `total` is 0 again on the next run, so accumulating into it never works. - Objects you built during the run — a fitted model, an open file handle, a big DataFrame — unless cached. - Any side effect you assumed would happen "once": without caching or a session-state guard, it happens on *every* interaction, including expensive queries. ## The st.button trap `st.button("Load")` returns `True` only on the run that the click triggered. If the user then touches any other widget, a new run happens and the button returns `False`, so anything you rendered inside `if st.button(...)` disappears. The fix is to record the decision rather than the click: ```python if st.button("Load"): st.session_state["loaded"] = True if st.session_state.get("loaded"): render_report() ``` The same reasoning explains why nesting one button inside another button's block almost never behaves as beginners expect. ## Reruns you trigger yourself You can force a rerun with `st.rerun()` (named `st.experimental_rerun` in older versions), typically after mutating session state so the rest of the page reflects the new state immediately. `st.stop()` ends the current run early — useful as a guard when required input is missing. Widget `on_change` / `on_click` callbacks do exist; they run *before* the rerun that follows the interaction, and their normal job is to update `st.session_state`, not to render output. ## The cost of the model, and how to pay it The rerun model buys simplicity and pays for it in repeated work. Two mitigations carry almost all the weight in practice: cache anything expensive (`st.cache_data` for data, `st.cache_resource` for connections and models), and reduce how often reruns happen — `st.form` batches several widgets so nothing reruns until the submit button is pressed, and a fragment reruns only its own function rather than the whole script. Understanding this one mechanism explains most Streamlit questions an interviewer will ask: why a variable resets, why the app hits the warehouse on every keystroke, why a button's output vanishes, and why session state exists at all.
- If the script reruns entirely, why doesn't the page flicker or lose scroll position?Streamlit does not reload the browser page. The rerun happens server-side in the same process, and the resulting elements are diffed against what is already on screen and patched over the existing websocket connection. The DOM is updated in place, so scroll position and unrelated widgets stay put.
- What is the difference between an on_change callback and just reading the widget's return value?The return value tells you the widget's state on this run and is the normal way to drive rendering. An on_change callback fires once, before the rerun that follows the interaction, and exists for side effects on session state — resetting a dependent widget, appending to a history list. If you only need the value, read the return value.
- How do you stop an expensive query from running on every keystroke in a text input?Three standard moves: wrap the query in st.cache_data so repeated calls with the same arguments are free, put the inputs inside an st.form so nothing reruns until submit, or gate the work behind a state flag set by a button. Caching plus a form covers most real cases.
It is closer to a spreadsheet recalculating every cell after an edit than to a desktop UI where a handler nudges one widget.
saying these in an interview costs you the question
- Thinks widgets fire callbacks and only that widget updates
- Says each rerun starts a brand-new Python process
- Accumulates into a module-level variable and expects it to grow
- Assumes code inside an if st.button block stays rendered
- Believes caching is optional polish rather than load-bearing