Data App Patterns¶
The tool-specific sheets teach widgets; this sheet teaches structure. These are the recurring decisions — which framework, how to split code, where state lives, how models get loaded — that separate a demo that dies in a notebook from an app people actually use.
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¶
Both frameworks are pip-installable and run as plain Python processes.
pip install streamlit gradio # or: uv add streamlit gradio
streamlit run app.py # http://localhost:8501
python gradio_app.py # http://127.0.0.1:7860
Core Syntax¶
Choosing a framework — the decision table¶
| Question | Streamlit | Gradio |
|---|---|---|
| Show data, tables, dashboards | ★★★★★ | ★★☆ |
| Full app: sidebar, tabs, multi-page | ★★★★★ | ★★★ |
| One function → demo in minutes | ★★★ | ★★★★★ |
| Upload an image → get prediction | ★★★ | ★★★★★ |
| Chat/LLM interfaces | ★★★★ (chat elements) | ★★★★★ (ChatInterface) |
| User must submit; minimal reruns | Forms, buttons | Events, buttons |
| Custom event wiring | Limited (script rerun model) | Full (click/change/submit) |
| Per-user state | st.session_state |
gr.State per session |
| Hosting | Streamlit Community Cloud | Hugging Face Spaces |
| Editable data grids | st.data_editor |
gr.Dataframe (less capable) |
Rule of thumb: data exploration and dashboards → Streamlit. Model demos, upload-and-infer, and chat prototypes → Gradio. Both can do either; pick the one whose mental model matches the job.
The shared mental model¶
Both apps are sessions around one Python process:
Differences that matter:
| Concern | Streamlit | Gradio |
|---|---|---|
| Execution model | Entire script reruns top-to-bottom per event | Only the bound handler runs |
| Persist values | st.session_state |
handler args + gr.State |
| Cache | @st.cache_data / @st.cache_resource |
load at module scope |
| Concurrency | one thread per session rerun | queue + worker pool |
| Sharing a quick demo | streamlit run + Cloud |
launch(share=True) one-liner |
Common Operations¶
Clean project layout (either framework)¶
app/ # NOT a single 800-line app.py
├── main.py # entry point: builds UI, wires events
├── ui/
│ ├── components.py # small reusable widgets/blocks
│ └── views.py # page sections
├── services/
│ ├── model_service.py # load + predict (framework-free)
│ ├── data_service.py # load/query data (framework-free)
│ └── llm_client.py # API clients
├── models/ # serialized models (gitignored, large)
├── assets/
├── .streamlit/secrets.toml # or .env / Space secrets
├── requirements.txt
└── README.md
Golden rule: keep UI code in main.py-side modules and everything domain-related (model loading, prediction, data access) in plain Python modules that don't import the framework. You can then unit-test services/ and reuse it in a FastAPI backend later.
Environment and secrets by platform¶
| Platform | Where secrets live | Reference |
|---|---|---|
| Streamlit local | .streamlit/secrets.toml |
st.secrets["api"]["KEY"] |
| Streamlit Cloud | Settings → Secrets (same TOML) | st.secrets[...] |
| Gradio local | os.environ / .env |
os.getenv("KEY") |
| HF Spaces | Settings → Variables and secrets | os.getenv("KEY") |
| Any container | --env-file / CI secrets |
os.getenv |
Never hard-code keys; never commit .env or secrets.toml.
Practical Examples¶
Example 1 — Demo-grade → production-grade model app¶
Demo: a script that loads the model, shows one prediction.
# demo_app.py
import joblib, streamlit as st
model = joblib.load("models/rf.joblib")
x = st.number_input("charges", 0.0, 500.0, 89.5)
if st.button("Predict"):
st.write(model.predict([[x]])[0])
Better: split concerns, add caching, validation, and error paths.
# services/model_service.py (no streamlit/gradio imports!)
import joblib
from functools import lru_cache
@lru_cache(maxsize=1)
def get_model():
return joblib.load("models/rf.joblib")
def predict(record: dict) -> dict:
expected = {"charges", "tenure", "contract"}
missing = expected - set(record)
if missing:
raise ValueError(f"missing fields: {sorted(missing)}")
row = [[record[f] for f in sorted(expected)]]
prob = float(get_model().predict_proba(row)[0, 1])
return {"prob": prob, "label": "churn" if prob > 0.5 else "retain"}
# main.py
import streamlit as st
from services.model_service import predict
st.title("Churn")
charges = st.number_input("charges", 0.0, 500.0, 89.5)
tenure = st.slider("tenure", 0, 120, 12)
contract = st.selectbox("contract", ["month-to-month", "one_year", "two_year"])
if st.button("Predict"):
try:
st.json(predict({"charges": charges, "tenure": tenure,
"contract": contract}))
except ValueError as e:
st.error(str(e))
The services module is importable by tests and by a future FastAPI route — zero rewrites.
Example 2 — Caching pyramid (Streamlit)¶
import streamlit as st
@st.cache_resource # once per process: models, clients
def load_model():
return joblib.load("model.joblib")
@st.cache_data(ttl=600) # once per (args): fetched/staged data
def load_features(date):
return query_warehouse(date)
@st.cache_data # derived results keyed by inputs
def compute_report(df, cutoff):
return df[df.value > cutoff].describe()
Order by cost: the most expensive and most stable things get the longest-lived caches.
Example 3 — Multi-page Streamlit app¶
# app.py — shared setup runs once per session
import streamlit as st
from services.model_service import get_model
st.set_page_config(page_title="Churn Suite", layout="wide")
if "model" not in st.session_state:
st.session_state.model = get_model() # loaded once, shared across pages
st.sidebar.markdown("# Churn Suite")
Each pages/*.py file becomes a sidebar page; they share st.session_state.
Example 4 — Chat app state pattern¶
# Streamlit: history lives in session_state
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"])
# ...append on each turn, re-render loop above shows it
# Gradio: history is threaded through the fn signature
def chat(message, history):
history = history or []
history.append({"role": "user", "content": message})
reply = call_llm(history)
history.append({"role": "assistant", "content": reply})
return "", history # clear box, return updated history
gr.ChatInterface(fn=chat, ...) # or gr.Chatbot + gr.State
Common Patterns¶
Session vs global state — know which you need¶
| Kind | Streamlit | Gradio | Lives |
|---|---|---|---|
| Per-user-per-session | st.session_state |
gr.State |
Browser session |
| Process-wide singleton | @st.cache_resource / module scope |
module scope | The server process |
| App-wide constants | module scope | module scope | Same |
Rule: anything that differs per user or per tab (chat history, selections, step progress) → session state. Anything shared and immutable (models, credentials, loaded corpora) → process-wide. Never put the model in session state — every user would reload it.
Loading models on a schedule / warm start¶
import threading, time
def _reload_loop():
while True:
time.sleep(3600)
refresh_model() # swap st.session_state.model / module var
threading.Thread(target=_reload_loop, daemon=True).start()
Good enough for internal apps; for real MLOps use a model registry and a versioned serving layer instead.
Progressive enhancement — demo becomes an API¶
Write services/ framework-free, then:
# api.py — same predict() function, FastAPI surface
from fastapi import FastAPI
from services.model_service import predict
app = FastAPI()
@app.post("/predict")
def api_predict(payload: dict):
return predict(payload)
The UI demo and the REST API now share one implementation — no drift between what the notebook demoed and what production serves.
Best Practices¶
- Split UI from logic. Framework-free
services/modules are testable and reusable. - Pick the framework by job: dashboards → Streamlit, demos → Gradio, and know when neither fits (then use a web framework instead).
- Cache aggressively: data loads, model loads, expensive derived frames.
- Validate inputs before calling the model; show clean errors, not tracebacks.
- Keep secrets out of code: platform secrets UI or
.env, referenced by name. - Structure for growth: multi-page Streamlit, named components in Gradio Blocks.
- Add a health/latency footer or
st.metricfor model version so stakeholders see what's deployed. - Write one smoke test that imports
services/and asserts a prediction shape. - Watch memory: cached big frames × sessions can exhaust a container — evict with
ttlor LRU.
Common Mistakes¶
| Mistake | Fix |
|---|---|
| Entire app in one file with imports interleaved with UI code | Split services/ from UI |
| Reloading a 2 GB model per rerun/per request | @st.cache_resource / module scope |
| Storing the model in session state | Process-wide cache — it's shared, not per-user |
Committing .env/secrets.toml |
.gitignore them; use platform secrets |
| 500 errors shown to users on bad input | Validate and return friendly errors |
Only ever st.write(df) for large data |
st.dataframe/gr.Dataframe with pagination |
| Two apps with identical logic (demo + API) drifting | One shared services/ layer |
| Thread-unsafe model shared across requests | Load per-process or guard with a lock / framework queue |
Troubleshooting¶
| Symptom | Cause / Fix |
|---|---|
| App works locally, crashes on Cloud/Space | Missing requirements.txt entry or missing secrets — check logs |
| Memory grows until restart | Cache without bounds — add ttl, reduce concurrency, stream data |
| Model gives stale predictions after retrain | Version the model path, restart, or add a reload loop |
| Slow first interaction | Cold model load — preload at boot (demo.load / warm-up call) |
| Sessions bleed into each other (user A sees user B's data) | Per-user data must live in session state, never a module global |
| UI code imports break when adding FastAPI backend | Refactor shared logic into services/ that imports nothing UI |
| Demo dies under traffic | Add demo.queue() / raise Streamlit Cloud resources; move heavy work behind a button |
Related Cheat Sheets¶
- Streamlit — widget reference and Cloud deployment
- Gradio — component reference and Spaces deployment
- FastAPI — the API surface for the same services
- HTML / CSS / JavaScript — when you graduate to a web app
- environment-variables — secrets everywhere
- git / github — source control + deploy triggers
- Docker — containerizing the app for any host
- pandas — the data layer inside handlers
- LLM — adding chat/intelligence patterns
Official Documentation¶
- Streamlit app architecture — https://docs.streamlit.io/develop/concepts/architecture
- Streamlit session state — https://docs.streamlit.io/develop/concepts/architecture/session-state
- Gradio Blocks guide — https://www.gradio.app/guides/blocks-and-event-listeners
- Gradio state — https://www.gradio.app/guides/state
Last Updated¶
2026-09-06 — Initial version.