cheat sheet
streamlit
Package-level reference for the streamlit framework on PyPI — install variants, version policy, extras, and alternatives.
streamlit
What it is
Streamlit is a Python web-app framework that turns plain Python scripts into shareable interactive dashboards without any HTML, CSS, or JavaScript. The project was open-sourced in 2019 and acquired by Snowflake in 2022; Snowflake continues to fund development while keeping the core Apache-2.0. It is the most-downloaded "data app" framework on PyPI by a wide margin.
Reach for streamlit when you want to ship a data dashboard, ML demo, or internal tool with the absolute minimum framework overhead — a 30-line app.py becomes a working web app. Reach for gradio for ML-input/output demos, reflex for proper SPA-style routing with state, or dash when you need detailed callback control.
Install
pip install streamlit
Output: (none — exits 0 on success)
uv add streamlit
Output: resolved + added to pyproject.toml
poetry add streamlit
Output: updated lockfile + virtualenv install
pipx install streamlit # global, isolated install for ad-hoc demos
Output: installs streamlit on PATH in a dedicated venv
streamlit hello # confirms the install with the bundled demo app
Output: opens http://localhost:8501 with the official Streamlit gallery
Versioning & Python support
- Current stable line is the
1.xseries (long-lived; Streamlit has been on1.xsince 2021). - Supports Python 3.9+ on recent releases; older releases support 3.8 and earlier.
- Loose semver —
1.xminor releases regularly add deprecations and occasionally remove APIs after a deprecation cycle. Watch the changelog when upgrading more than two minor versions. - Release cadence is roughly every 2–4 weeks.
Package metadata
- Maintainer: Snowflake (Streamlit Inc. team)
- Project home: github.com/streamlit/streamlit
- Docs: docs.streamlit.io
- PyPI: pypi.org/project/streamlit
- License: Apache-2.0
- Governance: Commercially backed by Snowflake; OSS contributions accepted
- First released: 2019
- Downloads: tens of millions per month — top PyPI dataviz framework
Optional dependencies & extras
Streamlit has no PyPI extras_require markers, but the install pulls in a sizeable dependency tree to power the UI:
tornado— the HTTP/WebSocket server runtimeprotobuf— wire protocol for backend ↔ frontend messagespyarrow— the table-serialization format forst.dataframepillow— forst.imagePIL inputspandas,numpy— pulled in for dataframe widgetsaltair— bundled default chart library forst.altair_chartwatchdog(optional) — faster file-change detection in dev; falls back to polling
Common companions:
streamlit-aggrid— sortable/editable tables with AG Gridstreamlit-authenticator— username + password auth on top ofsession_statestreamlit-extras— community-maintained widget packplotly,bokeh,pydeck— alternative chart engines, each with a dedicatedst.<lib>_chartcallstreamlit-folium— interactive Folium / Leaflet maps
Alternatives
| Package | Trade-off |
|---|---|
gradio | ML-demo-focused — strong "input → model → output" UX. Lighter than streamlit; less flexible for general dashboards. |
reflex | Full React app compiled from Python; proper routing, components, state. Heavier mental model and a Node.js build step. |
dash (plotly) | Callback-based React app. More control than streamlit; more boilerplate. |
panel (holoviz) | Pairs well with Jupyter + Bokeh ecosystem; flexible layout engine. |
nicegui | Vue.js-based, component-tree API. Closer to traditional web frameworks. |
voila | Turn a Jupyter notebook into a standalone app. Pure-notebook workflow. |
Common gotchas
- Top-to-bottom re-run on every widget interaction. The single most important model to internalize. Every button press, slider change, or text edit re-executes
app.pyfrom line 1. Heavy work must live behind@st.cache_data/@st.cache_resource. st.session_stateis the only way to persist values across re-runs. Module-level globals do persist within a single user session but are unsafe across users and re-runs — usest.session_stateinstead.- Streamlit Community Cloud is the smooth deploy target. Deploying to generic PaaS (Heroku, Fly, Render) works but is fiddly — WebSockets must be enabled, sticky sessions configured, and the entrypoint must be
streamlit run app.py --server.port=$PORT. - Files larger than
server.maxUploadSize(default 200 MB) silently fail. Configure in.streamlit/config.toml. - Multi-page apps require either a
pages/folder orst.navigation(newer API). The two APIs coexist and have slightly different URL semantics — pick one per project. @st.cache_datare-runs on hashable-argument change; unhashable args raise. Pass_underscored_argto skip hashing for that argument. Old@st.cachewas deprecated in 2022 — do not use it.- Dev server reloads aggressively. Saving any file in the working tree restarts the script; turn off via
runner.fastReruns = falseif it interferes with debugging.
Real-world recipes
Package-level recipes that emphasise deployment and integration. Widget APIs and layout primitives are in the companion Python article.
Recipe 1 — file uploader → cached parse → download
import streamlit as st
import pandas as pd
st.title("CSV summariser")
uploaded = st.file_uploader("Upload CSV", type=["csv"])
@st.cache_data
def parse(buf: bytes) -> pd.DataFrame:
return pd.read_csv(pd.io.common.BytesIO(buf))
if uploaded:
df = parse(uploaded.getvalue())
st.dataframe(df.head(20))
summary = df.describe().to_csv().encode()
st.download_button("Download summary", data=summary, file_name="summary.csv")
Output: uploader -> table -> download button. @st.cache_data keys on the bytes hash, so re-uploading the same file is free.
Recipe 2 — multipage app with pages/ folder
app.py
pages/
1_📊_Dashboard.py
2_🔧_Settings.py
3_📁_Data.py
Streamlit auto-discovers pages/*.py, sorts numerically, and renders the emoji as the icon. The leading number controls order; the rest of the filename (sans extension) becomes the page title.
Output: sidebar nav with three pages plus the home (app.py). No router code needed.
Recipe 3 — long-running task with st.spinner + cached resource
import streamlit as st
import time
@st.cache_resource
def get_model():
time.sleep(3) # imagine: load a 500 MB model from disk
return {"name": "fake-model"}
with st.spinner("Loading model..."):
model = get_model()
st.success(f"Loaded {model['name']}")
Output: spinner shows for 3 s on first run; instant on subsequent reruns because @st.cache_resource survives across reruns and across sessions. Use cache_resource for singletons (DB connections, ML models) and cache_data for values (DataFrames, JSON).
Recipe 4 — fragments for partial reruns (1.37+)
@st.fragment reruns only that fragment's code on widget interaction inside it — not the whole script. Use for self-contained sub-UIs that should not invalidate expensive top-of-script work.
import streamlit as st
@st.fragment
def filter_panel():
q = st.text_input("Filter")
st.write(f"Filtering by: {q}")
st.title("Dashboard")
filter_panel()
st.write("(heavy chart below would NOT rerun on filter changes)")
Output: typing in the filter input reruns only filter_panel(); the page title and the "heavy chart" lines are skipped. Fragments are the cleanest fix for the "everything reruns on every keystroke" complaint.
Recipe 5 — auth-gated app via st.session_state
import streamlit as st
def login():
with st.form("login"):
u = st.text_input("Username")
p = st.text_input("Password", type="password")
if st.form_submit_button("Sign in") and u == "alice" and p == "secret":
st.session_state["user"] = u
st.rerun()
if "user" not in st.session_state:
login()
st.stop()
st.write(f"Welcome, {st.session_state['user']}")
Output: form gates the app; after login, the rest of app.py runs. For production, replace the literal credentials with streamlit-authenticator, OAuth via streamlit-oauth, or a reverse-proxy auth layer (Cloudflare Access, Authelia).
Production deployment
Streamlit's deployment story splits cleanly along three lines: Streamlit Community Cloud (managed, free for public apps), Snowflake Streamlit-in-Snowflake (paid, isolated per Snowflake account), and self-hosted (any container platform).
Streamlit Community Cloud
The path of least resistance. Connect a GitHub repo, point at app.py, get an https://<slug>.streamlit.app URL. Free tier limitations:
- Public repos only (private repos require paid).
- ~1 GB RAM / app — fine for dashboards, not for ML inference.
- Apps spin down after ~7 days idle (warm-up on first hit).
- Limited build-time package install — no compiled extensions outside the
requirements.txtandpackages.txt(apt) format.
Pinning a specific Streamlit version in requirements.txt is mandatory — Community Cloud upgrades aggressively otherwise.
Self-host with a container
Minimal Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8501
ENV PYTHONUNBUFFERED=1
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health || exit 1
ENTRYPOINT ["streamlit", "run", "app.py", \
"--server.port=8501", \
"--server.address=0.0.0.0", \
"--server.enableCORS=false"]
Output: containerised Streamlit on port 8501. The /_stcore/health endpoint is Streamlit's built-in healthcheck for load balancers.
Reverse proxy + WebSocket
Streamlit uses WebSockets for bidirectional widget updates. The proxy must upgrade:
location / {
proxy_pass http://streamlit_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400; # long WebSocket idle
}
Output: Streamlit reaches the browser through Nginx with full WebSocket support. Common bug: forgetting the Upgrade header — the app loads but never refreshes on widget interaction.
Multi-user isolation
Streamlit serves one process per server; all browser tabs share the same Python interpreter. st.session_state isolates UI state per browser session, but module-level globals are shared across users — a per-user secret stored in a module global will leak to every other user.
For genuine multi-user isolation:
- Public dashboard, no user data — fine to share one process.
- Per-user data — use
st.session_stateexclusively; neverglobal. Authenticate via reverse proxy. - Strict isolation — one container per user (e.g., spawned via a Kubernetes Job or similar). Streamlit is not designed for this; consider
reflexor a FastAPI+React app.
Authentication
Streamlit has no built-in auth. Options ordered by sophistication:
- Reverse-proxy auth — Cloudflare Access, Authelia, oauth2-proxy. Sits in front of Streamlit; Streamlit sees authenticated requests only.
streamlit-authenticator— username/password stored in YAML, JWT cookies.streamlit-oauth— OAuth flows (Google, GitHub) inside the app.st.experimental_user(newer API) — built-in OIDC integration; check the docs for current stability.
Sticky sessions
If you scale Streamlit horizontally (multiple replicas), the load balancer must use session affinity (sticky sessions) so each browser's WebSocket lands on the same replica. Without it, widget updates fail silently across reconnects.
Performance tuning
Caching is the single biggest lever
@st.cache_data— caches values (DataFrames, dicts, parse results). Keyed by argument hash. Cache is per-process; survives reruns and sessions; cleared on file change. Use for expensive computations.@st.cache_resource— caches non-serialisable singletons (DB connections, ML models, large file handles). Same lifetime ascache_databut never copied; mutating the cached object mutates the cache. Use for "load once, reuse forever".
@st.cache_data(ttl=300, max_entries=50)
def fetch(symbol: str) -> dict:
return requests.get(f"https://api.example.com/{symbol}").json()
Output: cache entries expire after 5 min; the cache holds at most 50 distinct results. Both knobs are essential when the input space is unbounded.
Fragments reduce script-rerun cost
See Recipe 4. Wrap any sub-section whose widgets shouldn't re-run the top of the script.
Don't recreate matplotlib figures in a hot loop
A common anti-pattern:
# BAD — recreates the figure every rerun
fig, ax = plt.subplots()
ax.plot(data)
st.pyplot(fig)
If data is stable, wrap the plotting in @st.cache_data returning the fig. Streamlit re-renders, but matplotlib doesn't rebuild.
Avoid st.dataframe on million-row tables
st.dataframe serialises the entire frame to Arrow and ships it to the browser. For >100k rows, use server-side pagination:
page = st.number_input("Page", 0, len(df) // 1000)
st.dataframe(df.iloc[page * 1000 : (page + 1) * 1000])
Output: browser receives 1000 rows per interaction. For more, use streamlit-aggrid with virtualised rendering.
Version migration guide
Streamlit ships every 2-4 weeks. Each release usually adds at least one new widget / parameter; minor releases occasionally deprecate APIs. Compare to a stable framework (Django, FastAPI) and Streamlit feels churny.
Deprecated → removed timeline (recent)
@st.cache— deprecated 2022, removed in mid-2023. Use@st.cache_dataor@st.cache_resource.st.beta_*/st.experimental_*— many promoted (st.columns,st.expander,st.tabs); theexperimental_prefix is stripped over time. Old code usingst.experimental_singletonandst.experimental_memoshould migrate tocache_resource/cache_data.st.experimental_rerun→st.rerun(1.27+). The old name still works but emits a warning.session_statewas experimental, now stable. The API shape hasn't changed in a long time.
Multipage API split
pages/ folder (old) and st.navigation + st.Page (new) coexist. The new API gives programmatic control over the route table — preferable for apps with conditional pages or dynamic auth-gated routes. Existing pages/ apps continue to work indefinitely.
When upgrading
- Pin in
requirements.txt:streamlit==1.37.0(or whatever you tested). Floatingstreamlitis asking for a broken deploy. - Skim the release notes between your pinned and target versions — Streamlit's release notes are well-curated.
- Run the app locally first, click every widget; an integration-test stack is unusual but valuable (see below).
Security considerations
st.markdown(..., unsafe_allow_html=True) is genuinely unsafe
Any unescaped user input rendered this way is an XSS vector. The flag exists because the workflow community wants it; treat it as inline <script> injection.
# BAD — XSS if `user_input` came from a form
st.markdown(user_input, unsafe_allow_html=True)
# GOOD — Streamlit's built-in markdown is safe
st.markdown(user_input)
Output: the safe call renders Markdown without HTML; the unsafe call evaluates whatever HTML the user supplied.
Secrets
st.secrets reads from .streamlit/secrets.toml. Never commit this file. Add to .gitignore:
.streamlit/secrets.toml
On Streamlit Community Cloud, secrets are configured via the app settings UI and exposed at the same st.secrets interface — keep the local file out of git.
Authentication for public-facing apps
A Streamlit app on the public internet is a fully-executable Python sandbox to anyone with the URL. If the app has any sensitive functionality (database writes, file uploads to shared storage, expensive API calls), put authentication in front of it. The reverse-proxy approach is simplest; streamlit-oauth works for app-level auth.
File-upload safety
st.file_uploader returns a BytesIO. Treat it as untrusted bytes:
- Validate the magic header (
python-magic,imghdr). - Enforce size limits with
server.maxUploadSize. - Run any decoder (PIL, pandas) in a try/except — corrupt uploads can crash the worker.
- Never
open(uploaded.name, "wb")and write —uploaded.nameis user-controlled and can contain../.
WebSocket origin
Streamlit's default config accepts WebSocket connections from any origin. For embedded scenarios, restrict via server.enableCORS = false plus server.enableXsrfProtection = true (combined behaviour: the same-origin policy from the browser plus CSRF tokens from Streamlit).
Testing & CI integration
The streamlit-testing library (now streamlit.testing.v1, ships with Streamlit) runs an app in-process and asserts on widget state.
from streamlit.testing.v1 import AppTest
at = AppTest.from_file("app.py").run()
at.text_input[0].input("hello").run()
assert "hello" in at.markdown[0].value
Output: synthetic test; no browser, no real HTTP — Streamlit's runtime exposes the widget tree to the test harness directly. Works in any pytest setup.
For end-to-end tests against a real running app, use Playwright:
pip install playwright
playwright install chromium
Output: (none — exits 0 on success)
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("http://localhost:8501")
page.wait_for_selector("text=Hello")
page.fill('input[aria-label="Name"]', "alice")
page.locator('button', has_text="Submit").click()
assert page.locator("text=Welcome, alice").is_visible()
browser.close()
Output: runs the live Streamlit app with synthetic clicks. Useful for catching CSS / layout / WebSocket regressions that the in-process test can't see.
CI gotchas:
- The in-process tester does not exercise the JavaScript runtime — visual bugs slip through.
AppTestruns the script once per.run(); mutatingsession_statebetween assertions is fine.- Tests requiring
@st.cache_datainvalidation across runs: clear caches viaAppTest.clear_cache().
See also
- Python: streamlit — widgets, layout, session state, deployment
- Packages: pip-reflex — the SPA-style alternative
- Packages: pip-matplotlib — common chart backend