Most web frameworks ask you to learn HTTP. Streamlit asks you to write a Python script. x = st.slider("Select a value") followed by st.write(x, "squared is", x * x) is a complete interactive web app — run streamlit run streamlit_app.py and a browser opens with a live slider. The trick is a model no other framework uses: every user interaction throws the script away and reruns it top to bottom, and the framework diffs what changed, shipping only the differences to the browser. Widgets do not write event handlers; they return values. State does not live in a database or a server-side object graph; it lives in st.session_state, and caching with st.cache_data and st.cache_resource keeps the reruns fast. The README’s pitch — dashboards, reports, and chat apps in minutes instead of weeks — rests entirely on this rerun-everything architecture, which is why the codebase is worth reading: it is the most elegant execution of a radical idea in the Python data world.

The repository splits cleanly into three machines that pass protobuf messages between them. The Python runtime under lib/streamlit holds the ScriptRunner that executes your script in a managed thread, the AppSession that owns each browser tab’s state, and the delta generator that turns every st.* call into a Delta message. The wire protocol under proto/streamlit/proto defines ForwardMsg, BackMsg, and Delta — the vocabulary of the whole framework in three files. The React frontend under frontend receives those deltas and reconciles them into a live DOM, with a sophisticated dataframe layer on glide-data-grid underneath. Between them sits a Starlette server bridging WebSockets, static files, media, and uploads. Everything is Apache-2.0-licensed, installable with pip install streamlit, and tested by an extensive Playwright e2e suite. As always in this series, what follows is an educational tour of published source code.

Streamlit overview architecture diagram

Streamlit at a glance: the CLI bootstraps the runtime and Starlette server, the ScriptRunner executes your script top to bottom, every st.* call emits a Delta through the protobuf contract, the WebSocket bridge pushes ForwardMsgs to the React app, and widget events return as BackMsgs that update session state and trigger the next rerun.

Reading the overview from left to right:

Why You Need This

The first reason is the rerun model itself, which changes how you think about stateful apps. ScriptRunner at nine hundred lines owns the entire lifecycle: it executes the script in a dedicated thread through the exec_code module, catches exceptions into renderable error elements, enforces timeouts, and queues rerun requests so a widget firing mid-run schedules a clean next pass rather than corrupting the current one. The scripts are bytecode-cached in the script cache, and the magic module performs AST rewriting so that a bare df on the last line of a cell becomes an implicit st.write(df) — the notebook heritage showing through. The refinement of the blunt model is the fragment system, nine hundred lines letting a decorated function rerun alone without redrawing the page, which keeps the simple mental model while fixing its worst cost.

The second reason is the plumbing that makes reruns feel instant, which is a distributed-systems education in miniature. The AppSession at thirteen hundred lines is the state machine per browser tab: it owns session state, queues run requests, tracks uploaded files through the uploaded file manager, and registers media through the media file manager. Outbound messages flow through the forward message queue so a slow client cannot stall the script, and the forward message cache makes reconnects cheap by replaying cached widget state. Session state itself — eighteen hundred lines in session_state.py — is the subtlest file in the repo: it tracks widget identities across reruns, decides which values survive, handles stale widget cleanup, and syncs query parameters with the URL.

The third reason is the caching stack and the wire protocol, two designs worth copying into your own systems. st.cache_data and st.cache_resource share a structural hashing module that fingerprints arbitrary Python arguments — dataframes, models, functions, objects with cycles — into stable cache keys, with TTL sweeps, background refresh, and disk storage behind a unified API. On the wire, ForwardMsg wraps Delta messages that each describe one element, block, or metadata change at a path in the tree, while BackMsg carries widget updates and rerun requests back — a minimal, inspectable contract that keeps Python and TypeScript independent. The frontend closes the loop: App.tsx dispatches messages, the render tree applies deltas at their paths, and the dataframe layer renders Arrow tables at spreadsheet speed.

Streamlit detail architecture diagram

The detail view: the entry tier with CLI, bootstrap, config, and the Starlette server stack; the runtime core with sessions, message queues, caches, and file managers; the script execution tier with the runner, magic, fragments, and both caches; the state tier with session state, widget registry, query params, and the watcher; the protobuf contract in the middle; and the React frontend with its render tree and dataframe layer.

Walking the detail diagram, a session’s life is traceable end to end. The bootstrap module resolves the entrypoint, merges config from flags and environment through the twenty-eight-hundred-line config module, and starts the Starlette server, which serves the built frontend and media through the static routes and hands WebSocket connections to the websocket handler. Messages flow into the session manager and out to the right AppSession. Inside the run, the delta generator registers widgets in the widget registry, binds their values into session state, and serializes every element into a Delta.

Thererun side completes the circuit: a widget event arrives as a BackMsg, updates session state, and the execution control commands — st.rerun, st.stop — give the script itself vocabulary to steer its own lifecycle. During development, the path watcher monitors the entrypoint and imported modules so saving a file triggers the auto-rerun that makes the edit-see loop feel live. The two caches hook the same hashing core, so st.cache_resource can pin one loaded model across thousands of reruns while st.cache_data snapshots each dataframe transformation. What emerges is a framework where the concurrency lives entirely in the runtime, never in the user’s code — the reason a Streamlit app is simultaneously the easiest Python UI to write and, at the protocol level, one of the most carefully engineered reactive systems in the open source data stack.

From Install to Your First App

The path is two commands. pip install streamlit brings the wheel with everything included, and streamlit hello opens a gallery demo app to verify the install. Your first app is the slider script above; save it, run streamlit run streamlit_app.py, and the browser opens at localhost:8501. From there the API is deliberately shallow: widgets like st.button, st.selectbox, and st.file_uploader return values instead of registering callbacks; st.columns and st.tabs handle layout; st.dataframe renders Arrow-backed tables with sorting and the editable st.data_editor; st.chat_message and st.chat_input assemble conversational UIs, which is why Streamlit became the default frontend of the local-LLM world. Wrap expensive loads in st.cache_resource, wrap dataframe work in st.cache_data, pin page structure with st.fragment for partial reruns, and read state through st.session_state — the four idioms cover most apps. Everything runs locally by default; the same script deploys unchanged to Streamlit Community Cloud or any container that can run the CLI.

Honest limits: the rerun model is a ceiling as well as a floor. Apps where a widget change should not invalidate the whole page pay for simplicity in recomputation unless you reach for fragments and caching carefully, and scripts that mutate global state or depend on call order can behave surprisingly across reruns. Multi-user deployments scale one session per tab per process, so heavy apps need the containerized horizontal path rather than a bigger box. The framework hides HTTP so thoroughly that escaping it — custom auth, arbitrary routing, server push beyond session messages — means dropping below the abstraction the codebase works so hard to maintain. None of that changes the verdict: for the enormous space of data tools, dashboards, and AI demos, reading how Streamlit turns a script into an app is the fastest way to understand reactive UIs generally, and the rerun machine at its core is some of the cleanest systems code in Python.

Watch PyShine on YouTube

Contents