streamlit
Streamlit turns a plain Python script into an interactive web app: you write top-to-bottom Python with calls like st.slider and st.dataframe, run streamlit run app.py, and get a browser UI with live reload. The core mental model is unusual and non-negotiable: on every user interaction the entire script reruns from the top, and you manage persistence through st.session_state and the caching decorators. It is owned by Snowflake, ships a free Community Cloud for deployment, and has become the default way data scientists put a dashboard, internal tool, or LLM chat UI in front of people without writing JavaScript.
Unbeatable speed from Python script to working data app, which is exactly what it is for. Treat it as the prototype and internal-tool layer; if an app succeeds and needs real scale, custom UI, or embedding, that success is your signal to rebuild on a web framework.
Use it if
- You want a data app or internal dashboard in an afternoon, in pure Python, with widgets, charts, and dataframe display built in
- You are prototyping an LLM chat interface: st.chat_message, st.chat_input, and st.write_stream make a working chatbot UI in about 30 lines
- Your users are a team, not the internet: tens of concurrent viewers on an internal tool is exactly the load profile it handles well
- You need free hosting for demos: Community Cloud deploys straight from a GitHub repo
- You need to serve many concurrent users or fast interactions: every click reruns your whole script and each session holds a websocket plus server-side state, so it scales like a notebook, not like a web app
- You need precise UI control or your company's design system: layout is limited to Streamlit's widgets, columns, and theming, and anything custom means writing a React component against the components API
- You want to embed the UI inside an existing site or app: a Streamlit app is its own long-running server speaking over websockets, not a widget you can drop into another page (and corporate proxies that dislike websockets will hurt)
- You expected a lightweight dependency: pip install streamlit pulls in pandas, pyarrow, and a long tail of transitive packages; it is a heavy addition to a slim service image
- You plan to contribute features upstream: the project has paused accepting pull requests from outside the maintainer team
Setup reality
pip install streamlit && streamlit hello works in minutes, but note the install is heavy (pandas and pyarrow come along) and Python 3.10+ is required. The lasting cost is the execution model: until the rerun-everything loop clicks, you will fight duplicate-widget-key errors, state that resets when you expected it to stick, and expensive functions rerunning on every interaction until you wrap them in @st.cache_data or @st.cache_resource. Deployment means keeping a streamlit run process alive behind a websocket-friendly proxy (or handing the app to Community Cloud); it does not mount into gunicorn/WSGI like a normal Python web app.
Patterns
Smallest possible apphello-app
# streamlit_app.py
import streamlit as st
x = st.slider("Select a value")
st.write(x, "squared is", x * x)
# run with: streamlit run streamlit_app.pyThe whole script reruns on every slider move; that is the core execution model, not a bug.
Cache expensive data loadscache-data
import pandas as pd
import streamlit as st
@st.cache_data(ttl=3600)
def load_data(url: str) -> pd.DataFrame:
return pd.read_csv(url)
df = load_data("https://example.com/big.csv")
st.dataframe(df)cache_data returns a fresh copy per call and is keyed on the function's arguments; without it this CSV downloads again on every widget interaction.
Cache global resources (models, DB connections)cache-resource
import streamlit as st
@st.cache_resource
def get_model():
from sentence_transformers import SentenceTransformer
return SentenceTransformer("all-MiniLM-L6-v2")
model = get_model()cache_resource returns the same shared object to every session and rerun; use it for unpicklable things, and remember mutations are visible to all users.
Persist state across rerunssession-state
import streamlit as st
if "count" not in st.session_state:
st.session_state.count = 0
if st.button("Increment"):
st.session_state.count += 1
st.write("Count:", st.session_state.count)Plain variables reset on every rerun; session_state is per browser tab and dies with the session, it is not shared or persistent storage.
Batch inputs with a formform-batch-input
import streamlit as st
with st.form("signup"):
name = st.text_input("Name")
age = st.number_input("Age", min_value=0)
submitted = st.form_submit_button("Submit")
if submitted:
st.success(f"Saved {name}, {age}")Widgets inside a form do not trigger reruns until the submit button; this is the fix when every keystroke reruns an expensive script.
Upload and process a filefile-upload
import pandas as pd
import streamlit as st
uploaded = st.file_uploader("Choose a CSV", type="csv")
if uploaded is not None:
df = pd.read_csv(uploaded)
st.dataframe(df.describe())The value is None until a file is chosen, so guard for it; uploads live in memory with a 200 MB default limit (server.maxUploadSize).
LLM chat interface with streamingchat-ui
import streamlit as st
if "messages" not in st.session_state:
st.session_state.messages = []
for m in st.session_state.messages:
with st.chat_message(m["role"]):
st.markdown(m["content"])
if prompt := st.chat_input("Say something"):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
with st.chat_message("assistant"):
reply = st.write_stream(stream_from_your_llm(prompt))
st.session_state.messages.append({"role": "assistant", "content": reply})You must replay message history yourself on each rerun; st.write_stream accepts a generator and returns the full concatenated text.
Columns, sidebar, and tabslayout-columns
import streamlit as st
with st.sidebar:
mode = st.radio("Mode", ["Overview", "Detail"])
col1, col2 = st.columns(2)
col1.metric("Revenue", "$12k", "+8%")
col2.metric("Users", "430", "-2%")
tab1, tab2 = st.tabs(["Chart", "Raw data"])
with tab1:
st.line_chart(df)
with tab2:
st.dataframe(df)All tabs execute on every rerun even when not visible; heavy work inside a background tab still costs you.
Multi-page app with st.navigationmultipage-app
# app.py (entrypoint)
import streamlit as st
pages = st.navigation([
st.Page("pages_src/home.py", title="Home", icon=":material/home:"),
st.Page("pages_src/report.py", title="Report"),
])
pages.run()
# run with: streamlit run app.pyst.navigation/st.Page supersedes the old pages/ directory convention and gives programmatic control (auth-gated pages, dynamic menus).
Rerun only a fragment of the apppartial-rerun
import streamlit as st
@st.fragment(run_every="10s")
def live_prices():
st.dataframe(fetch_prices())
st.title("Dashboard") # not rerun by the fragment
live_prices()Interactions with widgets inside a fragment rerun only the fragment; run_every gives you polling without rerunning the whole script.
Keep API keys out of the scriptsecrets-config
# .streamlit/secrets.toml (never commit this)
# OPENAI_API_KEY = "sk-..."
import streamlit as st
key = st.secrets["OPENAI_API_KEY"]st.secrets reads .streamlit/secrets.toml locally and the app settings UI on Community Cloud; add the file to .gitignore before your first commit, not after.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| gradio | PyPI | You are demoing an ML model and want instant share links and Hugging Face Spaces hosting |
| dash | PyPI | Production analytics dashboards where callback-based control and scaling behavior matter more than authoring speed |
| panel | PyPI | You live in Jupyter notebooks or need to work across many plotting libraries with finer reactive control |