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
- Core Syntax
- Common Operations
- Practical Examples
- Common Patterns
- Best Practices
- Common Mistakes
- Troubleshooting
- Related Cheat Sheets
- Official Documentation
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; usest.secretsonly for API keys and server-side credentials.
Deploy to Streamlit Community Cloud¶
- Push
app.py+requirements.txtto a GitHub repo. - Log in at https://streamlit.io/cloud → New app → pick repo/branch/main file.
- Add secrets under Settings → Secrets (same TOML format as local).
- App runs at
<app-name>.streamlit.app; every git push redeploys.
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=Trueandst.metricfor 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() |
Related Cheat Sheets¶
- Gradio — the faster alternative for model demos
- data-app-patterns — architecture for real data apps
- HTML / CSS / JavaScript — if you outgrow Streamlit
- FastAPI — serving models behind the app
- pandas, Matplotlib, Plotly — the data + charts you render
- environment-variables — secrets and config
- git / github — pushing to deploy on Streamlit Cloud
Official Documentation¶
- Streamlit docs — https://docs.streamlit.io/
- Streamlit API reference — https://docs.streamlit.io/develop/api-reference
- Streamlit Community Cloud — https://docs.streamlit.io/deploy/streamlit-community-cloud
Last Updated¶
2026-09-06 — Initial version.