Skip to content

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

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:

browser event ──▶ handler ──▶ Python function ──▶ new component state ──▶ browser

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
pages/
├── 1_📊_Explore.py
├── 2_🧪_Predict.py
└── 3_⚙️_Settings.py
# 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.metric for 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 ttl or 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
  • 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

Last Updated

2026-09-06 — Initial version.