Skip to content

Streamlit

Turn Python scripts into interactive apps. Streamlit re-runs your script top-to-bottom on every interaction — you write plain Python, sprinkle in widgets, and get a web UI with zero HTML/JS.

Table of Contents

Installation / Setup

pip install streamlit          # or: uv add streamlit
streamlit run app.py           # opens http://localhost:8501
streamlit run app.py --server.port 8888

Create app.py with any Streamlit call and run it — the script becomes the app.

Core Syntax

Basic app

import streamlit as st

st.set_page_config(page_title="Churn Explorer", layout="wide")
st.title("Customer Churn Explorer")
st.write("Explore the churn dataset and score a customer.")

df = load_data()          # any function returning data
st.dataframe(df)

Text elements

Element Usage
st.title("...") / st.header / st.subheader Page + section headings
st.write(obj) Catch-all: renders strings, DataFrames, charts, dicts
st.markdown("**bold** withcode") Markdown with unsafe_allow_html=False default
st.caption("small text") Subtitles and notes
st.latex(r"\bar{x} = \frac{1}{n}\sum x_i") Math formulas
st.code("print(1)", language="python") Block of code
st.success/error/warning/info("msg") Colored status boxes
st.metric("Accuracy", "0.93", "+2.1%") KPI card with delta

Buttons & inputs

Widget Returns Notes
st.button("Predict") bool (True for one run) Use with a cache guard or session state
st.download_button("Save", data, file_name=...) bool Trigger a file download
st.text_input("Name", value="") str Single-line text
st.text_area("Prompt") str Multi-line text
st.number_input("Top K", min_value=1, max_value=100, value=5) int/float Numeric stepper
st.selectbox("Model", ["rf_v1", "lr_v1"], index=0) selected item Dropdown, one choice
st.multiselect("Features", options, default=options[:3]) list Dropdown, many choices
st.radio("Split", ["train", "test"]) item Radio buttons
st.slider("Threshold", 0.0, 1.0, 0.5, step=0.05) float Numeric slider
st.select_slider("Budget", options=["low","med","high"]) item Slider over arbitrary options
st.checkbox("Show outliers", value=False) bool Toggle
st.date_input("Date") / st.time_input("Time") date / time Date/time pickers
st.file_uploader("Upload CSV", type=["csv"]) UploadedFile Uploaded file object
st.color_picker("Line color", "#4F8BF9") hex str Color picker

Common Operations

Layout — columns, sidebar, tabs, expanders

left, right = st.columns(2)          # side-by-side containers
with left:
    st.button("Left button")
with right:
    st.text_input("Right field")

with st.sidebar:                     # sidebar: widgets + text
    st.header("Controls")
    model_name = st.selectbox("Model", models)
    threshold = st.slider("Threshold", 0.0, 1.0, 0.5)

tab1, tab2, tab3 = st.tabs(["Data", "Chart", "Predict"])
with tab1:
    st.dataframe(df)
with tab2:
    st.line_chart(df["value"])

with st.expander("Show code"):
    st.code("print('hidden until expanded')")

st.container()                       # logical grouping for ordering
st.empty()                           # placeholder you can fill/replace later

Forms — batch widget reads

with st.form("prediction_form"):
    tenure = st.number_input("Tenure (months)", 0, 120, 12)
    charges = st.number_input("Monthly charges", 0.0, 500.0, 89.5)
    contract = st.selectbox("Contract", ["month-to-month", "one_year", "two_year"])
    submitted = st.form_submit_button("Predict")

if submitted:                        # everything reads together, one rerun
    result = predict(tenure, charges, contract)
    st.write(result)

File upload + download

uploaded = st.file_uploader("Upload dataset", type=["csv", "parquet"])
if uploaded is not None:
    df = pd.read_csv(uploaded)       # file object works directly with pandas
    st.dataframe(df.head())

st.download_button(
    "Download predictions",
    data=csv_bytes,                  # bytes or str
    file_name="predictions.csv",
    mime="text/csv",
)

Tables & charts

st.dataframe(df)                     # interactive: sort, search, resize
st.dataframe(df, width=800, height=400)
st.data_editor(df)                   # editable grid — edits come back as df

st.table(df.head(5))                 # static table
st.line_chart(df, x="date", y="value")       # pandas convenience
st.area_chart(df, y=["a", "b"])
st.bar_chart(df, x="model", y="acc")
st.scatter_chart(pts, x="x", y="y", color="label")

import plotly.express as px          # or any Plotly/Altair/Matplotlib fig
fig = px.scatter(df, x="x", y="y", color="label")
st.plotly_chart(fig)
st.pyplot(matplotlib_fig)            # Matplotlib
st.altair_chart(alt_chart)
st.map(geo_df)                       # latitude/longitude points
st.image(img, caption="sample")      # images
st.audio(audio_bytes) / st.video(video_bytes)
st.audio(audio_bytes, format="audio/wav")

Practical Examples

Example 1 — Model scoring app with sidebar

import joblib
import pandas as pd
import streamlit as st

st.set_page_config(page_title="Churn Predictor", page_icon="📞")
model = joblib.load("churn_model.joblib")   # cached file load, once

st.title("Customer Churn Predictor")
st.write("Enter customer details to get a churn probability.")

with st.sidebar:
    st.header("About")
    st.caption("Demo model — not for production decisions.")

tenure = st.slider("Tenure (months)", 0, 120, 12)
charges = st.number_input("Monthly charges ($)", 0.0, 500.0, 89.5)
contract = st.selectbox("Contract", ["month-to-month", "one_year", "two_year"])

if st.button("Predict churn", type="primary"):
    row = pd.DataFrame([{
        "tenure": tenure, "monthly_charges": charges, "contract": contract,
    }])
    prob = float(model.predict_proba(row)[0, 1])
    st.metric("Churn probability", f"{prob:.1%}")
    st.success("High churn risk — offer a retention plan." if prob > 0.5
               else "Low churn risk.")

Example 2 — Explore an uploaded dataset

import pandas as pd
import streamlit as st
import plotly.express as px

st.title("Dataset Explorer")
uploaded = st.file_uploader("Upload a CSV", type=["csv"])
if uploaded is None:
    st.info("Upload a file to begin.")
    st.stop()                            # stops this run; reruns on upload

df = pd.read_csv(uploaded)
st.subheader("Preview")
st.dataframe(df.head(100))

numeric = df.select_dtypes("number").columns.tolist()
col = st.selectbox("Column to plot", numeric)
fig = px.histogram(df, x=col, nbins=30)
st.plotly_chart(fig, use_container_width=True)

Example 3 — Chat with an LLM (chat elements)

from openai import OpenAI
import streamlit as st

client = OpenAI(api_key=st.secrets["OPENAI_API_KEY"])

st.title("Doc Helper")
if "messages" not in st.session_state:
    st.session_state.messages = []

for msg in st.session_state.messages:      # render history
    with st.chat_message(msg["role"]):
        st.markdown(msg["content"])

prompt = st.chat_input("Ask about your data...")
if prompt:
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

    resp = client.chat.completions.create(
        model="gpt-4o-mini", messages=st.session_state.messages)
    answer = resp.choices[0].message.content
    st.session_state.messages.append({"role": "assistant", "content": answer})
    with st.chat_message("assistant"):
        st.markdown(answer)

Common Patterns

Session state — persist values across reruns

# init once
if "count" not in st.session_state:
    st.session_state.count = 0
    st.session_state.history = []

if st.button("Run"):
    st.session_state.count += 1
    st.session_state.history.append(...)

st.write("Runs:", st.session_state.count)

st.session_state is the only place values survive between widget interactions (every click reruns the whole script). Use it for chat history, computed results you want to keep, and multi-step wizards.

Caching — never recompute twice

@st.cache_data                      # for data & any serializable return
def load_data():
    return pd.read_parquet("data.parquet")

@st.cache_data(ttl=3600)            # expire after an hour
def fetch_from_api():
    return requests.get(URL).json()

@st.cache_resource                 # for models, DB connections, clients
def load_model():
    return joblib.load("model.joblib")

Cache inputs (function args) → cache outputs in memory; a hash of the args keys the cache. Invalidate with st.cache_data.clear() or change the function code.

Authentication concepts & secrets

# .streamlit/secrets.toml (never committed — add to .gitignore)
# [api]
# OPENAI_API_KEY = "sk-..."
# [auth]
# password = "change-me"
# username = "admin"

import streamlit as st
key = st.secrets["api"]["OPENAI_API_KEY"]     # available in Cloud + local

# simple login gate
def check_password():
    if st.session_state.get("authed"):
        return True
    with st.form("login"):
        user = st.text_input("Username")
        pwd = st.text_input("Password", type="password")
        if st.form_submit_button("Log in"):
            if (user == st.secrets["auth"]["username"]
                    and pwd == st.secrets["auth"]["password"]):
                st.session_state.authed = True
                st.rerun()
    return False

if check_password():
    st.title("Private dashboard")

Real apps: use external auth (OAuth via st.experimental_user, or an IdP) instead of shipping passwords in secrets. Never store client-visible data as secrets; use st.secrets only for API keys and server-side credentials.

Deploy to Streamlit Community Cloud

  1. Push app.py + requirements.txt to a GitHub repo.
  2. Log in at https://streamlit.io/cloud → New app → pick repo/branch/main file.
  3. Add secrets under Settings → Secrets (same TOML format as local).
  4. App runs at <app-name>.streamlit.app; every git push redeploys.
# requirements.txt
streamlit>=1.30
pandas
joblib
scikit-learn

Best Practices

  • Wrap file loads and model loads in @st.cache_data / @st.cache_resource — otherwise they rerun on every interaction.
  • Put global controls (model, threshold) in the sidebar; keep the main area for results.
  • Use forms for multi-widget submission so the app doesn't rerun on every keystroke.
  • Group heavy logic behind a single button (if st.button("Run")) when the step is slow.
  • Always give file uploads a type= allowlist; read with pandas directly from the upload object.
  • Give charts use_container_width=True and st.metric for headline numbers.
  • Validate user input and fail with st.error + st.stop() instead of a traceback.
  • Pin versions in requirements.txt; keep model files out of git and load them from blob storage or the repo if small.

Common Mistakes

Mistake Fix
No @st.cache_data — reloading a big CSV every rerun Cache data + model loading
Buttons fire twice (once per rerun) or logic runs before check Gate logic behind if st.button(...) / use forms
Widget value resets between reruns Store it in st.session_state
st.stop() missing → empty state crashes on None upload Early-return with st.info + st.stop()
Secrets committed to the repo Keep in .streamlit/secrets.toml, gitignored
Slow app because every widget triggers a full recompute Forms, caching, session state
Loading the model inside a widget branch Load at top level with @st.cache_resource

Troubleshooting

Symptom Cause / Fix
Widget changes do nothing You forgot to read the widget value or the code below runs before if
App reruns everything on every click (slow) Add caching; move slow work behind a button/form
FileNotFoundError for model/data in Cloud Model not deployed — bundle small files or fetch from storage
Secrets missing in Cloud Add them under app Settings → Secrets, matching your TOML
Port already in use streamlit run app.py --server.port 8502
Charts render blank Return the figure object; don't call .show()

Official Documentation

Last Updated

2026-09-06 — Initial version.