Ten lines of plain Python is all it takes to turn a data script into a web app your teammate can open in a browser. That is what this Streamlit tutorial shows you: no HTML, no JavaScript, no front-end framework. You write Python, add a few Streamlit calls, and it draws the buttons, sliders, charts, and tables. By the end you will have a data explorer with upload, filters, a live chart, and a metrics row, and know how to put it online for free.
“If you can’t ship it, it doesn’t exist. A model that only lives in your notebook is invisible to everyone but you.”
Every data team lead, eventually
Last Updated: July 2026 | Tested on: Python 3.14.6, Streamlit 1.59.1 | Difficulty: Intermediate | Reading Time: 19 minutes
Here is the situation Streamlit fixes. You wrote a script that loads a CSV, filters it, and draws a chart. It works great, on your machine. Now a colleague named Aditi wants to try it with her own numbers, and she does not run Python. Your options used to be ugly: teach her the terminal, or spend a week learning Flask, HTML forms, and a JavaScript charting library just to wrap 30 lines of analysis. Streamlit removes that whole detour. You keep writing Python, add a few st. calls where you want a widget, and Streamlit serves a real web page.
Think of it like the difference between cooking at home and running a small tiffin service. The recipe is the same. Streamlit is the counter, the menu board, and the serving window that lets other people order from your kitchen without stepping inside it.
Table of Contents
Prerequisites
You should be comfortable reading a pandas DataFrame and running a Python file from the terminal. Having seen a chart library like our Plotly tutorial helps, since we embed a Plotly figure later, but it is not required. You do not need any web development background for this Streamlit tutorial. That is the whole point.
Install and Verify
One install line gets you everything this Streamlit tutorial uses, including the small local web server the framework runs behind the scenes.
📄 install Streamlit
pip install streamlit
📄 verify_streamlit.py: confirm the install worked
import streamlit as st
print("Streamlit version:", st.__version__)
▶ Output
Streamlit version: 1.59.1
What happened here: A version number means you are ready. This post was tested on Streamlit 1.59.1. One thing to note early: you run a Streamlit app with streamlit run app.py, not with python app.py. Running it the plain way still executes the file, but there is no server and no browser page, just a warning about a missing script context. We will see the correct command in a moment.
Streamlit Tutorial Quick Win: A Data App in 30 Lines
Let us start with the smallest thing that feels like magic, in a good way: a real web page with a title, a slider, and a chart that redraws as you drag. Save this as hello.py.
📄 hello.py: your first Streamlit app
import streamlit as st
import numpy as np
st.title("My First Data App")
st.write("Drag the slider and watch the chart update.")
points = st.slider("How many points?", 10, 200, 50)
data = np.random.default_rng(0).normal(size=points).cumsum()
st.line_chart(data)
st.write(f"Plotted {points} points.")
Now run it with the Streamlit command, not with plain Python.
📄 start the app
streamlit run hello.py
▶ Output (terminal)
You can now view your Streamlit app in your browser. Local URL: http://localhost:8501 Network URL: http://192.168.1.56:8501
What happened here: Streamlit started a small web server and opened your browser to localhost:8501. The page shows a title, a sentence, a slider set to 50, and a line chart of 50 points. Drag the slider to 120 and the chart redraws with 120 points, no reload button, no page refresh. The Network URL line means anyone on the same WiFi can open the app on their phone too, which is handy for a quick demo across the desk. That is the quick win: ten lines of Python became a live, interactive page.
Build a Data Explorer: Upload, Filters, Charts, Metrics
A slider and a random chart is a nice hello, but the heart of this Streamlit tutorial is something you would actually hand to a colleague: a data explorer. It takes a CSV of food orders, lets the user filter by city and by minimum quantity, and shows a metrics row, a chart, and the filtered table. We will use a small orders dataset. Here is the CSV we generated for testing (240 rows across four cities), with the first few rows shown.
📄 make_data.py: create the sample orders CSV
import numpy as np, pandas as pd
rng = np.random.default_rng(7)
cities = ["Pune", "Mumbai", "Nagpur", "Nashik"]
items = ["Veg Thali", "Paneer Wrap", "Masala Dosa", "Chole Bhature"]
n = 240
df = pd.DataFrame({
"city": rng.choice(cities, n),
"item": rng.choice(items, n),
"quantity": rng.integers(1, 6, n),
"price": rng.choice([120, 90, 80, 110], n),
})
df["revenue"] = df["quantity"] * df["price"]
df.to_csv("orders.csv", index=False)
print("rows:", len(df))
print(df.head())
print("total revenue:", int(df["revenue"].sum()))
▶ Output
rows: 240
city item quantity price revenue
0 Nashik Chole Bhature 5 110 550
1 Nagpur Veg Thali 2 90 180
2 Nagpur Chole Bhature 1 90 90
3 Nashik Masala Dosa 2 80 160
4 Nagpur Paneer Wrap 5 110 550
total revenue: 70310
Now the app itself. Save this as app.py in the same folder as orders.csv, then run streamlit run app.py.
📄 app.py: the data explorer
import pandas as pd
import streamlit as st
st.title("Veg Orders Explorer")
@st.cache_data
def load(file):
return pd.read_csv(file)
# Upload your own CSV, or fall back to the sample file
upload = st.file_uploader("Upload orders CSV", type="csv")
df = load(upload) if upload else load("orders.csv")
# Filters live in the sidebar
city = st.sidebar.selectbox("City", ["All"] + sorted(df["city"].unique()))
min_qty = st.sidebar.slider("Minimum quantity", 1, 5, 2)
view = df[df["quantity"] >= min_qty]
if city != "All":
view = view[view["city"] == city]
# A metrics row: three numbers side by side
c1, c2, c3 = st.columns(3)
c1.metric("Revenue", f"Rs {int(view['revenue'].sum()):,}")
c2.metric("Orders", len(view))
c3.metric("Avg order", f"Rs {view['revenue'].mean():.0f}")
# A chart and the filtered table
st.line_chart(view.groupby("item")["revenue"].sum())
st.dataframe(view)
The whole app is about 20 lines. To show you exactly what those metrics and the chart compute, here is the same filtering and aggregation run as a plain script. Picture a user who selected Pune in the sidebar and pushed the minimum quantity slider to 2.
📄 metrics.py: the numbers behind the widgets
import pandas as pd
df = pd.read_csv("orders.csv")
# The user picked "Pune" and set the slider to 2
city, min_qty = "Pune", 2
view = df[(df["city"] == city) & (df["quantity"] >= min_qty)]
print(f"City filter: {city}, min quantity: {min_qty}")
print(f"Rows after filter: {len(view)}")
print(f"Total revenue: {int(view['revenue'].sum())}")
print(f"Average order value: {view['revenue'].mean():.1f}")
print(f"Top item by revenue: {view.groupby('item')['revenue'].sum().idxmax()}")
print()
print("Revenue by item:")
print(view.groupby("item")["revenue"].sum().sort_values(ascending=False))
▶ Output
City filter: Pune, min quantity: 2 Rows after filter: 39 Total revenue: 13840 Average order value: 354.9 Top item by revenue: Masala Dosa Revenue by item: item Masala Dosa 4120 Veg Thali 4060 Paneer Wrap 2880 Chole Bhature 2780 Name: revenue, dtype: int64
What happened here: Every widget in the app is just a normal Python value. st.slider returned the integer 2, st.selectbox returned the string "Pune", and the rest is plain pandas: filter the rows, sum the revenue, group by item. In the app, the metrics row shows Rs 13,840 revenue across 39 orders with an average of Rs 355, and the line chart is that “Revenue by item” series. When the user drags the slider to 3, Streamlit hands your code a new number, the whole thing recomputes, and the page updates. You never wrote a single line of JavaScript or an HTML form.
Want an interactive chart with hover tooltips instead of the built-in st.line_chart? Streamlit embeds Plotly figures directly. Build the figure the way you would in our Plotly tutorial, then hand it to st.plotly_chart(fig) and you get zoom, pan, and hover inside your app.
Caching and the Rerun Model
Here is the one idea that trips up everybody new to Streamlit, and once it clicks the whole framework makes sense. Every time the user touches a widget, Streamlit reruns your entire script from the first line to the last. There is no callback, no event handler, no “on click” function. The slider moved? Run the whole file again, top to bottom, with the slider now returning its new value. If you keep one thing from this Streamlit tutorial, keep that rerun model.
Think of it like a spreadsheet. Change one cell and the whole sheet recalculates, every formula, instantly. Streamlit works the same way: your script is the set of formulas, and any widget change triggers a full recalculation. This is why the code reads top to bottom like a normal script with no event plumbing, which is lovely. The catch is obvious too: if your script reads a big file or trains a model on line 3, that slow work runs again on every single click. That is what caching fixes.
You wrap any expensive function in @st.cache_data. Streamlit remembers the result for a given set of inputs. Next time the script reruns with the same inputs, it skips the work and hands back the stored result. Here is that behavior measured directly.
📄 cachetest.py: cache_data skips repeated work
import time
import streamlit as st
@st.cache_data
def load_orders(n):
time.sleep(2) # pretend this is a slow file read
return list(range(n))
t0 = time.perf_counter()
load_orders(1000)
first = time.perf_counter() - t0
t0 = time.perf_counter()
load_orders(1000) # same input, so the cache answers
second = time.perf_counter() - t0
print(f"First call: {first:.3f}s (ran the function)")
print(f"Second call: {second:.5f}s (served from cache)")
▶ Output
First call: 2.001s (ran the function) Second call: 0.00023s (served from cache)
What happened here: The first call actually ran the slow function and took the full two seconds. The second call, with the same input 1000, never entered the function body at all. Streamlit matched the input, returned the cached list, and finished in a fraction of a millisecond. In a real app that reads a 50 MB CSV, this is the difference between a snappy app and one that freezes for two seconds on every click.
Change the input, say load_orders(2000), and Streamlit sees a new argument and runs the function again, caching that result separately. There is a sibling decorator, @st.cache_resource, for things you want to create once and share, like a database connection or a loaded ML model, rather than copy per user.
Layout: Columns, Tabs, Sidebar, Forms
By default Streamlit stacks everything in one tall column, top to bottom, in the order your code runs. That is fine for a quick tool, but real dashboards need structure. You get four building blocks: the sidebar for controls, columns for side-by-side content, tabs for separate views, and forms for batching input. Here is how each one reads in code.
📄 layout.py: the four layout building blocks
import streamlit as st
# 1. Sidebar: put filters and controls out of the main flow
name = st.sidebar.text_input("Your name")
# 2. Columns: three metrics side by side
c1, c2, c3 = st.columns(3)
c1.metric("Orders", 39)
c2.metric("Revenue", "Rs 13,840")
c3.metric("Avg", "Rs 355")
# 3. Tabs: split content into named views
chart_tab, data_tab = st.tabs(["Chart", "Raw data"])
with chart_tab:
st.write("A chart goes here")
with data_tab:
st.write("A table goes here")
# 4. Form: collect several inputs, then submit once
with st.form("order_form"):
city = st.selectbox("City", ["Pune", "Mumbai", "Nagpur"])
qty = st.number_input("Quantity", 1, 20, 1)
submitted = st.form_submit_button("Add order")
if submitted:
st.success(f"Added {qty} order(s) for {city}")
What happened here: Anything you attach to st.sidebar lands in the left panel, which keeps your filters tidy and away from the results. st.columns(3) returns three column objects you write into with c1.metric(...), so the three numbers sit in a row instead of stacking. Tabs hide content until clicked, which is perfect for a “Chart” view and a “Raw data” view of the same query. The form is the important one for performance: normally every keystroke or click triggers a full rerun, but widgets inside st.form hold their values and only fire the rerun when the user clicks the submit button.
That is exactly what you want when someone is filling in five fields and you do not want the app recomputing after each one.
Deploy to Streamlit Community Cloud
An app on localhost only helps you. The whole reason we did this was so Aditi can open it from her own laptop. Streamlit Community Cloud hosts public apps for free at the time of writing, straight from a GitHub repository. The flow is short, and it is the payoff step of this Streamlit tutorial.
- Push your project to a public GitHub repo. It needs your
app.pyand arequirements.txtlisting your libraries (for this app:streamlit,pandas). - Go to share.streamlit.io, sign in with GitHub, and click “New app”.
- Pick the repo, branch, and the path to
app.py, then click Deploy. - After a minute you get a public URL like
https://your-app.streamlit.appthat anyone can open. Push a new commit and it redeploys on its own.
The one file people forget is requirements.txt. Locally your app works because you already installed the libraries. The cloud starts from an empty machine and reads that file to know what to install. Generate a minimal one by hand so you do not accidentally pin your entire environment.
📄 requirements.txt: what the cloud installs
streamlit pandas
Streamlit Community Cloud is the zero-effort option, but it is not the only one. You can containerize the app with Docker and run it on any server, or host it on a platform like Hugging Face Spaces, which also runs Streamlit apps. The free tier limits and exact steps for any host change over time, so check the provider’s current docs before you rely on them for anything important.
The Same App in Gradio
No Streamlit tutorial is complete without the closest alternative, because the two tools shine in different spots. Gradio is another pure-Python UI library, and it is built around one idea: you have a function, and you want a web UI with inputs on one side and outputs on the other. That framing makes Gradio the natural fit for machine learning demos, wrapping a model’s predict function so people can try it. It is also the default for sharing demos on Hugging Face Spaces. Here is our orders explorer rebuilt in Gradio.
📄 gradio_app.py: the same explorer, Gradio style
import pandas as pd
import gradio as gr
df = pd.read_csv("orders.csv")
def explore(city, min_qty):
view = df[df["quantity"] >= min_qty]
if city != "All":
view = view[view["city"] == city]
revenue = int(view["revenue"].sum())
by_item = view.groupby("item")["revenue"].sum().reset_index()
return f"Rs {revenue:,}", by_item
demo = gr.Interface(
fn=explore,
inputs=[gr.Dropdown(["All", "Pune", "Mumbai", "Nagpur", "Nashik"], value="All"),
gr.Slider(1, 5, value=2, step=1)],
outputs=[gr.Text(label="Revenue"), gr.Dataframe(label="Revenue by item")],
title="Veg Orders Explorer",
)
if __name__ == "__main__":
demo.launch()
▶ Output (terminal)
* Running on local URL: http://127.0.0.1:7860 * To create a public link, set `share=True` in `launch()`.
What happened here: Notice the shape of the code. In Gradio you write one plain function, explore(city, min_qty), and describe its inputs and outputs. Gradio builds the page around that function: dropdown and slider on the left, revenue text and table on the right. Run it and you get a local URL, and passing share=True to launch() gives you a temporary public link with no deploy step at all. That function-in, result-out model is why Gradio feels so clean for a “try my model” demo. Streamlit’s rerun-the-whole-script model, on the other hand, fits multi-step dashboards and data apps with lots of interacting widgets better. Many teams use both: Streamlit for internal dashboards, Gradio for model demos on Hugging Face.
If you outgrow both, the next tier up is a full callback-based framework like Dash (from the Plotly team) or Panel, where you wire explicit callbacks and get fine control over a large multi-page app. Reach for those when Streamlit’s “rerun everything” model starts to feel limiting, not before.
Common Mistakes
Mistake 1: Running the app with python instead of streamlit run
You type python app.py out of habit, the script runs, nothing opens, and you get a warning about a missing ScriptRunContext. Streamlit apps are not run by the Python interpreter directly. They are run by the Streamlit Command-Line Interface (CLI), which starts the web server and wires up the widgets.
✅ The fix
# Wrong: runs the file but starts no server python app.py # Right: starts the server and opens the browser streamlit run app.py
Mistake 2: Reloading a big file on every interaction
Because the whole script reruns on every click, an unguarded pd.read_csv("big.csv") at the top reloads the file every single time the user moves a slider. The app feels slow and you cannot figure out why. The answer is almost always a missing cache decorator on the load function.
✅ The fix
# Slow: reruns on every widget change
def load():
return pd.read_csv("big.csv")
# Fast: runs once, then served from cache
@st.cache_data
def load():
return pd.read_csv("big.csv")
Why: The rerun model is what makes Streamlit code so simple, but it means any slow line runs on every interaction. Wrapping the load in @st.cache_data tells Streamlit to do the work once and reuse the result, as we measured earlier: two seconds down to a fraction of a millisecond.
Mistake 3: Expecting variables to persist between reruns
You write count = count + 1 hoping to keep a running total across clicks. It resets to the same value every time, because each rerun starts the script fresh and your plain variable is born again. Values that must survive between reruns live in st.session_state, a dictionary Streamlit keeps for you across reruns of the same session.
✅ The fix
import streamlit as st
# Persist a value across reruns with session_state
if "count" not in st.session_state:
st.session_state.count = 0
if st.button("Add one"):
st.session_state.count += 1
st.write("Count:", st.session_state.count)
Why: A normal variable is reset on every rerun, so it can never accumulate. st.session_state is the memory that outlives a rerun, so a counter, a shopping list, or a chat history all belong there. Anything you want to remember from one click to the next goes in session state, not a plain variable.
Best Practices
- DO wrap every slow function (file reads, API calls, queries) in
@st.cache_data, and use@st.cache_resourcefor one-time objects like a model or a database connection. - DO keep filters and controls in
st.sidebarso the main area stays focused on results. - DO use
st.formwhen several inputs should be submitted together, so the app reruns once instead of after every field. - DO commit a small, hand-written
requirements.txtbefore deploying, listing only the libraries your app actually imports. - DON’T reach for Streamlit when you need a highly custom, pixel-perfect front end. A rerun-based framework trades control for speed, and past a point a real web framework fits better.
- DON’T store secrets like API keys in your code. Use Streamlit’s secrets management (
st.secrets) or environment variables so nothing sensitive lands in your public repo.
Conclusion
In this Streamlit tutorial you turned a plain Python script into a web app people can actually use. You built a data explorer with file upload, sidebar filters, a metrics row, and a live chart, all in about 20 lines. You learned the one idea that makes Streamlit click, the top-to-bottom rerun on every interaction, and you fixed its cost with @st.cache_data. You laid out columns, tabs, and forms, pushed the app to a free public URL, and saw the same explorer rebuilt in Gradio so you know when to reach for each. That is enough to ship a real internal tool this week.
From here, wire your app to a live source instead of a static CSV, or wrap one of your own models in a Gradio demo. For the full path from Python basics to machine learning, head back to the Python + AI/ML tutorial series home and keep going in order.
Frequently Asked Questions
Do I need to know HTML, CSS, or JavaScript to use Streamlit?
No. That is the main appeal of this streamlit tutorial: you write only Python. Streamlit turns your st.slider, st.line_chart, and st.dataframe calls into a real web page with the HTML, CSS, and JavaScript generated for you. You can add custom HTML later if you want, but you never have to.
Why does my Streamlit script run from top to bottom on every click?
That is Streamlit’s core rerun model. Instead of callbacks, any widget interaction reruns your whole script with the widgets returning their new values, much like a spreadsheet recalculating when one cell changes. It keeps the code simple. Wrap slow functions in @st.cache_data so they do not repeat the work on every rerun.
Is Streamlit free, and can I host apps for free?
The Streamlit library is open source under the Apache 2.0 license and free to use. Streamlit Community Cloud also hosts public apps for free from a GitHub repo at the time of writing. For private or higher-traffic apps you can self-host with Docker or use a paid tier. Check the current hosting docs, since free-tier limits change.
Streamlit vs Gradio: which should I use?
Use Streamlit for multi-widget dashboards and data apps where several controls interact. Use Gradio when you want to wrap a single function, especially a machine learning model’s predict call, into a quick input-output demo, or when you are publishing on Hugging Face Spaces. Both are pure Python, and many teams use both for different jobs.
How do I keep a value between interactions in Streamlit?
Use st.session_state, a dictionary Streamlit preserves across reruns of the same session. A plain Python variable resets on every rerun, so counters, chat history, or a running list must live in st.session_state to survive from one click to the next.
Interview Questions on Streamlit
Try each one aloud first. The phrasing you produce under mild pressure is what interviews measure.
Q: Explain Streamlit’s execution model. What happens when a user moves a slider?
Streamlit reruns the entire script from top to bottom on every widget interaction. There are no callbacks: when the slider moves, the whole file executes again, and this time st.slider returns the new value. The page is rebuilt from that fresh run. It works like a spreadsheet recalculating when one cell changes, which keeps the code linear and easy to read, at the cost of rerunning any slow work unless you cache it.
Q: What does @st.cache_data do, and how is it different from @st.cache_resource?
@st.cache_data memoizes the return value of a function keyed by its inputs, so an expensive load or computation runs once and later reruns get the stored result. It is meant for data like DataFrames and returns a fresh copy each time so callers cannot corrupt the cache. @st.cache_resource is for objects you want to create once and share by reference, like a database connection or a loaded ML model, rather than copy per call.
Q: A colleague says their Streamlit counter variable never increases past one. What is wrong?
They are using a plain Python variable, which is recreated on every rerun, so it resets instead of accumulating. State that must survive between reruns belongs in st.session_state, the dictionary Streamlit preserves for the session. Initialize the counter there once with an if "count" not in st.session_state guard, then increment st.session_state.count.
Q: When would you choose Gradio over Streamlit?
Gradio is built around wrapping a single function into an input-output UI, which makes it the natural pick for a machine learning model demo where you expose one predict call. It is also the default for Hugging Face Spaces. Streamlit fits multi-widget dashboards and data apps where many controls interact and the layout is richer. They are not rivals so much as tools for different shapes of app.
Q: What do you need in a repo to deploy to Streamlit Community Cloud, and what is the file people most often forget?
You need a public GitHub repo with the app script and a requirements.txt listing the libraries the app imports. The forgotten file is almost always requirements.txt: the app runs locally because the libraries are already installed, but the cloud starts from an empty machine and installs only what that file lists. Missing it causes a ModuleNotFoundError on deploy.
Q: Why should you wrap file loading in a cache decorator in a Streamlit app?
Because the script reruns top to bottom on every interaction, an uncached pd.read_csv reloads the file each time the user touches any widget, making the app feel sluggish on large files. Wrapping the load in @st.cache_data runs it once and serves the stored DataFrame afterward, turning a repeated multi-second read into a near-instant cache hit.
Related Posts
Previous: Plotly: Interactive Visualizations & Dashboards
Next: Python: Matplotlib vs Seaborn vs Plotly, Visualization Libraries Compared
Series Home: Python + AI/ML Tutorial Series

No comment