Streamlit: the fastest path from a Python script to a live data app

Streamlit: the fastest path from a Python script to a live data app

Every data scientist, ML engineer, and analyst hits the same wall at some point: you have a Python script that produces something useful — a model prediction, a cleaned dataset, a chart — but nobody else can use it. The script lives on your laptop. Your manager does not have Python installed. Your client wants a URL they can open in a browser. The traditional solutions are painful: learn React, wire up a Flask API, deploy a Vue frontend, fight with CSS. Streamlit exists to make all of that disappear.

Streamlit is an open-source Python framework that lets you turn data scripts into shareable web apps in minutes — with no frontend experience required. Write Python, get a web app. That is the entire value proposition.

This article is a comprehensive, opinionated tour of everything Streamlit offers: the execution model that makes it unique, every major widget and layout primitive, caching and state management, multi-page apps, custom components, theming, security, deployment, and a realistic comparison against the alternatives. If you are evaluating Streamlit for a project, this should be the last guide you need.


What Streamlit actually is (and what it is not)

Streamlit is a script-first reactive framework for building data web apps. You write a Python script top-to-bottom, and Streamlit turns each line into a browser element. There is no HTML template, no CSS file, no JavaScript bundle to manage. The script is the app.

Streamlit is:

Streamlit is not a general-purpose web framework. You would not build a full-stack SaaS product with it. It is specifically designed for data-centric interactive apps — dashboards, model demos, data explorers, reporting tools, internal utilities. If your primary output is JavaScript-heavy UI, you want something else.

Getting started: installation and your first app

Installation is one command:

pip install streamlit

Create a file called app.py:

import streamlit as st

st.title("My first Streamlit app")
name = st.text_input("What is your name?")
if name:
    st.write(f"Hello, {name}! 👋")

number = st.slider("Pick a number", 0, 100, 50)
st.write(f"You chose: {number}")

Run it:

streamlit run app.py

Your browser opens at http://localhost:8501 with a live, interactive app. Change a slider, and the page re-renders instantly. Every element on screen was produced by that tiny script. That is the entire development loop — edit, save, see.


The execution model: why Streamlit works differently

This is the single most important concept in Streamlit, and the reason it behaves unlike any other web framework you have used. When a user interacts with a widget — types text, clicks a button, moves a slider — Streamlit does not call a handler function. It reruns the entire script from top to bottom.

Every interaction triggers a full script rerun. Your script is the app. There is no callback, no event loop, no virtual DOM. The script runs from line one to the end, and whatever it outputs is what the user sees.

This design has three consequences that matter:

  1. Deterministic rendering. The UI is always a faithful reflection of the script's current state. There is no "stale view" problem — what you see is what the script just produced.
  2. Widget values persist across reruns. Streamlit automatically tracks which widget produced what value, so a slider remembers its position between reruns without you writing any state-management code.
  3. Performance requires explicit caching. Because the whole script runs on every interaction, expensive operations (database queries, model inference, large file reads) will re-execute on every rerun unless you explicitly cache them. This is the one area where Streamlit demands deliberate thought.

Every widget type: inputs, outputs, and interactive elements

Streamlit ships a large library of built-in widgets. Every widget is a function call — no configuration objects, no component lifecycle. Here is the full inventory, grouped by purpose:

Text and number inputs

Sliders, selectors, and date pickers

File upload and media

Buttons, forms, and execution control

The st.form() is particularly important for UX: without it, every keystroke in a text input triggers a full script rerun. Wrapping a group of inputs in a form defers the rerun until the user clicks the submit button, dramatically reducing server load and improving responsiveness.

Output and data display

Native charting

Streamlit has built-in wrappers for the most popular Python charting libraries:


Layout: columns, sidebar, tabs, and containers

Streamlit gives you several layout primitives to organize content beyond the default single-column layout:

Additional layout helpers include st.divider() (horizontal rule), st.caption(text) (small muted text), st.markdown(text) (full Markdown support including headers, bold, italic, links, and even HTML with unsafe_allow_html=True).

Styling and custom CSS

Streamlit has a default look that is clean but not customizable through API parameters. To override it, you inject custom CSS:

st.markdown(
    """
    <style>
    .stApp { background-color: #0e1117; }
    h1 { color: #00d4aa; }
    </style>
    """,
    unsafe_allow_html=True,
)

For deeper theming, Streamlit supports a .streamlit/config.toml file that controls the theme globally:

[theme]
primaryColor = "#00d4aa"
backgroundColor = "#0e1117"
secondaryBackgroundColor = "#1a1f2e"
textColor = "#fafafa"
font = "monospace"

You can also set the base to "dark" or "light", and choose from available fonts: "sans serif", "serif", "monospace", or "sans".


Caching: st.cache_data and st.cache_resource

Because Streamlit reruns the entire script on every interaction, any expensive computation will re-execute unless you cache it. Streamlit provides two caching decorators, and choosing the right one matters:

A common mistake: using @st.cache_data for a database connection. The connection object is not serializable and may break across sessions. Use @st.cache_resource instead.

@st.cache_data(ttl=3600)  # refresh every hour
def load_data(url: str) -> pd.DataFrame:
    return pd.read_csv(url)

@st.cache_resource
def get_db_connection():
    return sqlite3.connect("data.db")

Both decorators support a ttl parameter (time-to-live in seconds) for automatic expiration, and max_entries for bounding memory usage.


Session state: persisting data across reruns

Widget values (slider positions, text inputs) are automatically preserved across reruns — Streamlit handles that internally. But any custom variable you compute or store needs st.session_state to persist across reruns.

import streamlit as st

# Initialize
count = st.session_state.get("count", 0)

if st.button("Increment"):
    st.session_state.count = count + 1
    st.rerun()

st.write(f"Count: {st.session_state.get('count', 0)}")

Session state is a dictionary-like object scoped to each browser tab. Key properties:

The pattern is simple: read from st.session_state at the top of your script, update it when events happen, and the state persists until the session ends. For multi-page apps, this is how you share data between pages.


Fragments: partial reruns for better performance

One of Streamlit's newer features (introduced in v1.33+) is the @st.fragment decorator. It lets you define a function that reruns independently from the rest of the script:

@st.fragment
def live_chart():
    chart_data = fetch_recent_data()
    st.line_chart(chart_data)

# This function reruns on its own schedule
# without triggering a full-page rerun
live_chart()

# The rest of the app stays stable
st.write("This does not rerun when the chart updates")

Fragments solve the biggest performance problem in Streamlit: in a complex dashboard, every widget interaction triggers a full rerun. With fragments, you can isolate sections that update frequently (live charts, streaming data) without rerunning the entire app. This is a significant improvement for production dashboards.


Dialogs: modal popups with @st.dialog

Streamlit's @st.dialog decorator creates modal dialogs that overlay the app:

@st.dialog("Confirm action")
def confirm_dialog(message: str):
    st.write(message)
    if st.button("OK"):
        st.session_state.confirmed = True
        st.rerun()
    if st.button("Cancel"):
        st.rerun()

if st.button("Delete file"):
    confirm_dialog("Are you sure you want to delete this file?")

Dialogs run in a separate execution context — the rest of the app continues running while the dialog is open. This is useful for confirmation flows, multi-step wizards, and any UI that requires a focused interaction.


Multi-page apps: navigation and routing

Streamlit supports multi-page apps through two mechanisms:

1. Auto-discovery (the simple way)

Create a pages/ directory in your project folder. Any Python file you put in there automatically becomes a page in a sidebar navigation menu. The filename becomes the page title (underscores are converted to spaces).

my_app/
├── app.py              # main entry point
└── pages/
    ├── 1_📊_Analytics.py
    ├── 2_📈_Forecasting.py
    └── 3_⚙️_Settings.py

2. Explicit navigation (the controlled way)

For full control over page ordering, titles, and URL paths, use st.navigation() and st.Page():

import streamlit as st

pg = st.navigation([
    st.Page("home.py", title="Home", url_path="home", default=True),
    st.Page("settings.py", title="Preferences", url_path="settings"),
    st.Page("pages/about.py", title="About Us"),
])

pg.run()

You can also use st.switch_page("pages/my_page.py") to navigate programmatically, and st.rerun() to restart the current page.


Custom components: extending Streamlit beyond its built-ins

When Streamlit's built-in widgets are not enough, you have three options:

  1. Third-party components. The Streamlit component ecosystem includes hundreds of community-built widgets: streamlit-aggrid (advanced data grids), streamlit-elements (Material UI components), streamlit-ace (code editor), streamlit-extras (decorative elements, trees, mermaid diagrams, and more).
  2. iframe injection. For quick integrations, you can embed any external web app or widget using st.components.v1.html(iframe_code) or st.iframe(url).
  3. Building your own. Streamlit components can be written in Python (for simple cases) or as React/TypeScript frontends with Python backends. The framework exposes declare_component for Python-only components and components.declare_component for full bidirectional communication with a React frontend.

The Streamlit community has published over 600 components on the official gallery at streamlit.io/components. If you need a specific widget, check there first.


Data connections: databases, APIs, and secrets

Streamlit provides a st.connection abstraction for managing data sources:

For secrets management, Streamlit reads from a .streamlit/secrets.toml file locally (gitignored by default) and from environment variables in production. Access them with st.secrets["api_key"].

# .streamlit/secrets.toml
db_password = "super-secret"
api_key = "sk-..."

On Streamlit Community Cloud, secrets are set through the web UI under "Advanced Settings" and are never exposed in your code repository.


Performance optimization: patterns that matter

The script-rerun model means that performance is the primary engineering challenge in any non-trivial Streamlit app. Here are the patterns that actually make a difference:

  1. Cache aggressively. Any function that reads a file, queries a database, or calls an API should be wrapped in @st.cache_data or @st.cache_resource. Without this, every slider movement will re-execute your data pipeline.
  2. Use forms for batch input. Wrap multiple text inputs, sliders, and selectors in st.form() to defer reruns until the user clicks "Submit."
  3. Use fragments for live updates. Isolate frequently-updating sections with @st.fragment so they rerun independently.
  4. Lazy-load heavy data. Do not load a 2GB dataset at the top of the script. Load it in a function, cache it, and call it only when the user actually needs it (e.g. after clicking a button).
  5. Avoid global mutable state. Because the script reruns from scratch, any mutation of a global variable that should persist must go into st.session_state. Otherwise the mutation is lost on the next rerun.
  6. Use containers for conditional rendering. st.empty() and st.container() let you show/hide entire sections without restructuring the page.

Security considerations

Streamlit apps are web applications exposed to the internet. Security is your responsibility, and several areas deserve attention:


Deployment: from localhost to production

Streamlit provides multiple deployment paths, each with different tradeoffs:

Streamlit Community Cloud (free)

The fastest path: connect a GitHub repository and deploy in seconds. Community Cloud is free for public repositories and offers one free private app. It handles HTTPS, custom domains (for paid plans), and automatic redeployment on push. Limitations: apps sleep after 15 minutes of inactivity, and resource limits (1 GB RAM, basic CPU) make it unsuitable for heavy compute.

Docker (self-hosted)

For full control, package your app in a Docker container:

# Dockerfile
FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .
EXPOSE 8501

HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
ENTRYPOINT ["streamlit", "run", "app.py", \
    "--server.port=8501", \
    "--server.address=0.0.0.0"]

This gives you full control over resources, networking, and scaling. Deploy on any cloud provider (AWS ECS, Google Cloud Run, Azure Container Apps) or your own infrastructure.

Kubernetes (enterprise)

For large-scale deployments, Streamlit supports Kubernetes. Key considerations: each session requires a persistent WebSocket connection (so session affinity matters), and horizontal scaling requires a shared cache backend (Redis) if using @st.cache_data across replicas.

Other options


The Streamlit ecosystem: libraries and tools

Streamlit's value compounds when combined with the right libraries:


Streamlit vs. the alternatives: Dash, Gradio, Panel

Three competitors dominate the Python web-app space. Each serves a different sweet spot:

Streamlit vs. Plotly Dash

Dash (by Plotly) is the enterprise heavyweight. It uses a callback-based model — you define explicit functions that trigger when specific inputs change — rather than Streamlit's full-rerun approach. Dash gives you more control, especially for complex interactivity, but demands more boilerplate. For most data-science and internal-tool use cases, Streamlit is faster to build; for large, production-grade apps with many cross-dependencies, Dash's callback graph can be more predictable at scale.

Streamlit vs. Gradio

Gradio is optimized for one specific thing: ML model demos. If you have a function that takes input (text, image, audio) and produces output, Gradio's gr.Interface() wraps it in a demo page with one line. It has excellent Hugging Face integration (deploy any Gradio app to HF Spaces) and supports 40+ component types for diverse data (images, audio, video, 3D models). Streamlit is more flexible for building full dashboards and multi-page apps; Gradio is simpler for single-function model demos.

Streamlit vs. Panel

Panel (by HoloViz) is the most customizable of the four. It supports both the notebook-first approach (define widgets in a notebook, serve them as an app) and a full scripting approach. Panel's strength is in scientific visualization: it integrates deeply with Bokeh, Matplotlib, and HoloViews, and its reactive programming model (@param.depends) is more granular than Streamlit's rerun model. The tradeoff: Panel has a steeper learning curve and less community support than Streamlit.

When to pick what


Where Streamlit falls short (honest limitations)

No framework is perfect. Here is where Streamlit's design creates real friction:


Best practices for production Streamlit apps

  1. Structure your project. Do not put everything in one file. Use modules for data loading, model inference, and visualization. Import them into your Streamlit script. This makes testing, caching, and reuse much easier.
  2. Use type hints. Streamlit uses function signatures for some features (component registration). But even for regular functions, type hints improve readability and enable IDE support in a codebase that looks deceptively simple.
  3. Pin your dependencies. Use a requirements.txt or pyproject.toml with exact versions. Streamlit updates frequently, and a new version can subtly change widget behavior.
  4. Test your data pipeline separately. Your data loading and transformation logic should be testable without Streamlit. Put it in pure Python functions, test with pytest, and only call it from the Streamlit script.
  5. Add error handling. Use st.error(), st.warning(), and try/except blocks to gracefully handle failures. A crashed Streamlit app shows the raw exception to the user.
  6. Use the health check endpoint. Streamlit exposes /_stcore/health — use it in your Docker HEALTHCHECK and monitoring.
  7. Monitor with built-in metrics. Streamlit exposes Prometheus metrics at /_stcore/metrics. Integrate with Grafana or Datadog for production monitoring.

Quick reference: the Streamlit API cheat sheet

# App configuration
st.set_page_config(page_title="My App", layout="wide", initial_sidebar_state="expanded")

# Navigation
st.switch_page("pages/settings.py")
pg = st.navigation([st.Page("page1.py"), st.Page("page2.py")])
pg.run()

# Inputs
text = st.text_input("Name")
num = st.number_input("Age", 0, 120)
slider = st.slider("Score", 0, 100)
option = st.selectbox("Pick", ["A", "B", "C"])
files = st.file_uploader("Upload CSV", type=["csv"])

# Layout
col1, col2 = st.columns([3, 1])
with col1:
    st.write("Main content")
tab1, tab2 = st.tabs(["Data", "Chart"])
with st.expander("Details"):
    st.write("Hidden content")

# State
st.session_state.my_var = "value"
st.session_state.get("my_var", "default")

# Caching
@st.cache_data(ttl=3600)
def load(): ...
@st.cache_resource
def get_model(): ...

# Fragments
@st.fragment
def live_section(): ...

# Dialog
@st.dialog("Title")
def my_dialog(): ...

# Execution
st.rerun()          # restart the script
st.stop()           # halt execution here

# Output
st.write("Hello")
st.dataframe(df)
st.metric("Revenue", "$1.2M", "+12%")
st.pyplot(fig)
st.plotly_chart(fig)

The bottom line

Streamlit occupies a specific and valuable niche: the fastest way to get a Python data script into a browser that other people can use. Its script-rerun model is both its superpower (remarkably simple mental model) and its Achilles heel (performance at scale demands explicit caching and fragment optimization). For internal tools, dashboards, ML demos, and data-exploration apps, it is hard to beat the speed from idea to running app. For production systems with complex interactivity, fine-grained state management, or enterprise auth requirements, you will eventually want the additional control that Dash or Panel provides.

The Streamlit ecosystem is mature, well-documented, and actively maintained by Snowflake (which acquired Streamlit in 2023). With native support for Polars, Plotly, and the broader Python data stack, plus growing features like fragments, dialogs, and improved caching, it remains one of the best starting points for any Python developer who needs to turn code into a web app.


Sources