The idea
Most web frameworks make you write "when this button is clicked, update that label". Streamlit doesn't. Its model is much simpler, and slightly surprising:
Every time a user interacts with a widget, Streamlit reruns your whole script from top to bottom, with the widget now returning its new value.
That's it. No callbacks to wire up, no update functions. Your script is a recipe for the page, and Streamlit cooks it again whenever an ingredient changes. Understanding this one rule explains nearly everything Streamlit does, including the parts that confuse beginners.
Running an app on your computer
On this site, the App tab runs your script in a simulator. On your own machine, Streamlit is a normal Python package. With uv:
uv add streamlit
uv run streamlit run app.py
Streamlit starts a small local web server and opens http://localhost:8501 in your browser. When you save app.py, the page offers to rerun; choose Always rerun and every save refreshes the app instantly. The terminal shows any errors, and so does the page itself.
Watching the rerun happen
The best way to understand reruns is to see one. This tiny app prints the exact time it ran, then asks for your name. Keep an eye on the time while you use it:
import streamlit as st
from datetime import datetime
st.title("Rerun demo")
st.write("This script ran at", datetime.now().strftime("%H:%M:%S.%f")[:-3])
name = st.text_input("Your name")
st.write(f"Hello, {name or 'stranger'}!")
Press Try it, then type a name and press Enter. The time changes: the whole script ran again, not just the greeting line. Typing into the box didn't "update the greeting"; it triggered a fresh run in which st.text_input returned your text.
Widgets return values
Because of the rerun model, a widget is just a function call that returns the current value:
import streamlit as st
discount = st.slider("Discount (%)", 0, 50, 10)
price = 320_000
st.write(f"Price after discount: Rp {price * (1 - discount / 100):,.0f}")
On the first run, the slider returns its default (10). When the user drags it to 25, the script reruns and st.slider returns 25. Your code never asks "did the slider change?"; it just uses discount like any variable.
Buttons are True for exactly one run
A button returns True only on the run triggered by its click, and False on every other run:
import streamlit as st
if st.button("Refresh data"):
st.success("Data refreshed!")
st.slider("Unrelated slider", 0, 10, 5)
Click Refresh data: the success message appears. Now move the slider: the message disappears, because this new run wasn't caused by the button. That's correct behaviour, but it surprises people who expect the message to "stay".
The counter that doesn't count
Here's the most famous beginner bug:
import streamlit as st
count = 0
if st.button("Add one"):
count += 1
st.write("Count:", count)
Click three times: it always shows 1. Every run starts the script from the top, so count = 0 runs again; the click adds one, and that's it. Normal variables don't survive between runs. To remember values, Streamlit has st.session_state, which gets its own module soon.
Stopping early: st.stop
Sometimes the rest of the page makes no sense until the user provides input. st.stop() ends the current run right there:
import streamlit as st
st.title("Customer lookup")
email = st.text_input("Customer email")
if not email:
st.info("Enter an email address to start.")
st.stop()
st.write(f"Looking up orders for **{email}**...")
Without st.stop(), you'd need to indent the rest of the app inside an if. Stopping early keeps the main code flat and readable, the same "guard clause" idea from the Python functions module.
What the rerun model means for you
| Consequence | What to do |
|---|---|
| Every interaction reruns everything | keep the script fast; slow steps (loading files, queries) need caching |
| Normal variables reset each run | store things to remember in st.session_state |
Buttons are True for one run | use them for actions; use checkboxes/toggles for on/off state |
| The page is rebuilt from scratch | write the script top to bottom in the order the page should appear |
The next modules cover each of these tools. For now, the important thing is the mental model: script in, page out, again and again.
Why this matters for clean code
The rerun model rewards clean, linear scripts: read inputs at the top, compute, then display. Apps written that way are easy to follow because the code order is the page order. When an app gets confusing, it's usually because it fights the model (trying to "update" things) instead of embracing it (recompute everything from the current inputs).
Recap
- Run apps locally with
uv run streamlit run app.py. - Any widget interaction reruns the whole script from the top.
- Widgets are functions that return their current value.
- Buttons return
Trueonly on the run their click triggered. - Normal variables reset every run;
st.session_state(later) remembers values. st.stop()ends the run early; use it as a guard clause.