cheat sheet

streamlit

Package-level reference for the streamlit framework on PyPI — install variants, version policy, extras, and alternatives.

#pip#package#web#dataviz#uiupdated 05-31-2026

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

bash
pip install streamlit

Output: (none — exits 0 on success)

bash
uv add streamlit

Output: resolved + added to pyproject.toml

bash
poetry add streamlit

Output: updated lockfile + virtualenv install

bash
pipx install streamlit   # global, isolated install for ad-hoc demos

Output: installs streamlit on PATH in a dedicated venv

bash
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.x series (long-lived; Streamlit has been on 1.x since 2021).
  • Supports Python 3.9+ on recent releases; older releases support 3.8 and earlier.
  • Loose semver — 1.x minor 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 runtime
  • protobuf — wire protocol for backend ↔ frontend messages
  • pyarrow — the table-serialization format for st.dataframe
  • pillow — for st.image PIL inputs
  • pandas, numpy — pulled in for dataframe widgets
  • altair — bundled default chart library for st.altair_chart
  • watchdog (optional) — faster file-change detection in dev; falls back to polling

Common companions:

  • streamlit-aggrid — sortable/editable tables with AG Grid
  • streamlit-authenticator — username + password auth on top of session_state
  • streamlit-extras — community-maintained widget pack
  • plotly, bokeh, pydeck — alternative chart engines, each with a dedicated st.<lib>_chart call
  • streamlit-folium — interactive Folium / Leaflet maps

Alternatives

PackageTrade-off
gradioML-demo-focused — strong "input → model → output" UX. Lighter than streamlit; less flexible for general dashboards.
reflexFull 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.
niceguiVue.js-based, component-tree API. Closer to traditional web frameworks.
voilaTurn a Jupyter notebook into a standalone app. Pure-notebook workflow.

Common gotchas

  1. 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.py from line 1. Heavy work must live behind @st.cache_data / @st.cache_resource.
  2. st.session_state is 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 — use st.session_state instead.
  3. 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.
  4. Files larger than server.maxUploadSize (default 200 MB) silently fail. Configure in .streamlit/config.toml.
  5. Multi-page apps require either a pages/ folder or st.navigation (newer API). The two APIs coexist and have slightly different URL semantics — pick one per project.
  6. @st.cache_data re-runs on hashable-argument change; unhashable args raise. Pass _underscored_arg to skip hashing for that argument. Old @st.cache was deprecated in 2022 — do not use it.
  7. Dev server reloads aggressively. Saving any file in the working tree restarts the script; turn off via runner.fastReruns = false if 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

python
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

text
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

python
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.

python
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

python
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.txt and packages.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:

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:

nginx
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_state exclusively; never global. 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 reflex or a FastAPI+React app.

Authentication

Streamlit has no built-in auth. Options ordered by sophistication:

  1. Reverse-proxy auth — Cloudflare Access, Authelia, oauth2-proxy. Sits in front of Streamlit; Streamlit sees authenticated requests only.
  2. streamlit-authenticator — username/password stored in YAML, JWT cookies.
  3. streamlit-oauth — OAuth flows (Google, GitHub) inside the app.
  4. 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 as cache_data but never copied; mutating the cached object mutates the cache. Use for "load once, reuse forever".
python
@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:

python
# 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:

python
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_data or @st.cache_resource.
  • st.beta_* / st.experimental_* — many promoted (st.columns, st.expander, st.tabs); the experimental_ prefix is stripped over time. Old code using st.experimental_singleton and st.experimental_memo should migrate to cache_resource / cache_data.
  • st.experimental_rerunst.rerun (1.27+). The old name still works but emits a warning.
  • session_state was 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). Floating streamlit is 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.

python
# 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:

text
.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.name is 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.

python
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:

bash
pip install playwright
playwright install chromium

Output: (none — exits 0 on success)

python
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.
  • AppTest runs the script once per .run(); mutating session_state between assertions is fine.
  • Tests requiring @st.cache_data invalidation across runs: clear caches via AppTest.clear_cache().

See also