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.
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.
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.
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:
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:
st.text_input(label) — single-line text field. Returns a string.st.text_area(label) — multi-line text area. Useful for long-form input.st.number_input(label, min_val, max_val) — numeric input with stepper arrows and min/max bounds.st.password_input(label) — text input that masks the characters (replaced the old type="password" parameter).st.slider(label, min, max, value) — single value slider or st.slider(label, min, max, value=(low, high)) for a range slider.st.select_slider(label, options) — slider with discrete options instead of numeric values.st.selectbox(label, options) — dropdown selector. Returns the selected value.st.multiselect(label, options) — multi-select dropdown. Returns a list.st.radio(label, options) — radio button group.st.checkbox(label) — single checkbox. Returns True or False.st.date_input(label) — date picker.st.time_input(label) — time picker.st.file_uploader(label, type) — drag-and-drop file upload. Accepts type filter (e.g. ["csv", "xlsx"]).st.camera_input(label) — capture an image from the user's webcam.st.audio(data) — embed an audio player.st.video(data) — embed a video player.st.image(image) — display an image from a file path, URL, NumPy array, or PIL Image.st.button(label) — click button. Returns True only on the rerun immediately after the click (True for one rerun).st.form_submit_button(label) — submit button inside a form (widgets inside a form only submit together).st.form(key) — groups widgets so they only rerun the script when submitted, not on every keystroke.st.download_button(label, data, file_name) — presents a download link for data (string, bytes, or file).st.link_button(label, url) — renders an external hyperlink as a button.st.toggle(label) — on/off toggle. Like a checkbox but visually a switch.st.popover(label) — a button that opens a small popover panel with content inside.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.
st.dataframe(df) — interactive table with sorting, filtering, search, and column resizing. Supports Pandas, Polars, and Arrow.st.data_editor(df) — editable version of st.dataframe. Users can add/delete/reorder rows, edit cells, and insert columns. Returns the edited dataframe.st.table(df) — static, non-interactive table (rarely used in practice).st.metric(label, value, delta) — big number card with optional delta indicator. Designed for KPI dashboards.st.json(obj) — pretty-printed, collapsible JSON tree.st.code(code, language) — syntax-highlighted code block with a copy button.st.latex(expr) — renders LaTeX mathematical expressions.st.progress(value) — progress bar.st.status(label, state) — expandable status container with "running", "complete", or "error" states.st.toast(message) — ephemeral toast notification (no page layout disruption).st.balloons() — celebratory balloon animation. Yes, really.st.snow() — snowflakes. For the holidays.Streamlit has built-in wrappers for the most popular Python charting libraries:
st.line_chart(df) — line chart powered by Apache ECharts.st.area_chart(df) — stacked area chart.st.bar_chart(df) — bar chart.st.scatter_chart(df) — scatter plot.st.map(df) — geographic map (latitude/longitude columns auto-detected). Uses Deck.gl.st.pyplot(fig) for Matplotlib, st.plotly_chart(fig) for Plotly, st.bokeh_chart(fig) for Bokeh, st.altair_chart(chart) for Altair.Streamlit gives you several layout primitives to organize content beyond the default single-column layout:
st.sidebar — a permanent sidebar panel on the left. Any widget placed here is separate from the main content area. This is where most apps put filters and configuration.col1, col2, col3 = st.columns(3) — splits the page into columns (any number). Widths can be adjusted with the ratio parameter (e.g. st.columns([2, 1, 1]) for a 50-25-25 split).st.tabs(["A", "B", "C"]) — tabbed layout. Each tab is a context manager: content written inside a tab only appears in that tab.st.expander("Details") — collapsible section. Hidden by default, expands on click.st.container() — invisible grouping container. Useful for conditional rendering or applying styles to a group of elements.st.empty() — a placeholder that can be overwritten. Call placeholder.empty() to clear it, or placeholder.write("new") to replace its contents.st.columns(spec, vertical_alignment) — supports vertical_alignment="top", "center", or "bottom" to align columns vertically.st.container(border=True) — renders with a visible border, useful for creating card-like sections.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).
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".
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:
@st.cache_data — for functions that return serializable data (strings, numbers, DataFrames, dicts, lists). It hashes the function's arguments, runs the function once, serializes the result to disk, and returns the cached copy on subsequent calls. Each user session gets its own copy (safe from cross-session mutation).@st.cache_resource — for functions that return non-serializable objects (database connections, ML models, file handles). Returns the same object to all sessions (shared). This is the right choice for database connection pools or loaded model objects.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.
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:
on_change callback that can modify session state before the rerun completes.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.
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.
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.
Streamlit supports multi-page apps through two mechanisms:
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
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.
When Streamlit's built-in widgets are not enough, you have three options:
streamlit-aggrid (advanced data grids), streamlit-elements (Material UI components), streamlit-ace (code editor), streamlit-extras (decorative elements, trees, mermaid diagrams, and more).st.components.v1.html(iframe_code) or st.iframe(url).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.
Streamlit provides a st.connection abstraction for managing data sources:
st.connection("sql", type=SQLConnection) — connects to any SQL database (PostgreSQL, MySQL, SQLite, BigQuery, Snowflake, Databricks).st.connection("snowflake", type=SnowflakeConnection) — native Snowflake integration.st.connections.BaseConnection.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.
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:
@st.cache_data or @st.cache_resource. Without this, every slider movement will re-execute your data pipeline.st.form() to defer reruns until the user clicks "Submit."@st.fragment so they rerun independently.st.session_state. Otherwise the mutation is lost on the next rerun.st.empty() and st.container() let you show/hide entire sections without restructuring the page.Streamlit apps are web applications exposed to the internet. Security is your responsibility, and several areas deserve attention:
st.secrets and environment variables.@st.cache_resource, the cached object is shared — be careful with mutable shared state.streamlit-authenticator library for username/password flows. For Streamlit Community Cloud, you can restrict access by email address.st.markdown(..., unsafe_allow_html=True), never pass user-controlled input directly. Always sanitize it to prevent XSS attacks.st.file_uploader to specific file types and size limits. Uploaded files are stored in a temp directory — clean them up if they are sensitive.Streamlit provides multiple deployment paths, each with different tradeoffs:
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.
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.
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.
Streamlit's value compounds when combined with the right libraries:
st.dataframe and st.data_editor have native Polars support — no conversion needed. For large datasets, Polars' lazy evaluation pairs perfectly with Streamlit's caching to deliver fast, interactive exploration.st.plotly_chart(fig, use_container_width=True) renders fully interactive Plotly charts with zoom, hover, and selection — the most popular visualization choice for Streamlit dashboards.st.map() uses Deck.gl under the hood. For custom maps, use st.pydeck_chart() directly with PyDeck layer specifications.pip install streamlit-authenticator ) that adds username/password authentication with cookie-based sessions. Popular for internal tools.Three competitors dominate the Python web-app space. Each serves a different sweet spot:
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.
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.
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.
No framework is perfect. Here is where Streamlit's design creates real friction:
requirements.txt or pyproject.toml with exact versions. Streamlit updates frequently, and a new version can subtly change widget behavior.st.error(), st.warning(), and try/except blocks to gracefully handle failures. A crashed Streamlit app shows the raw exception to the user./_stcore/health — use it in your Docker HEALTHCHECK and monitoring./_stcore/metrics. Integrate with Grafana or Datadog for production monitoring.# 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)
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.