Streamlit Tutorial: Turn a Python Script into a Data App

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.

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.

Inputs unchangedFirst run ornew inputsUser opens the appin a browserStreamlit runs thescript top to bottomFunction wrapped inst.cache_data?Skip the work,return cached resultRun the function,store result in cacheWidgets draw and returntheir current valuesPage renders:metrics, charts, tablesUser moves a slideror picks a new valueStreamlit’s Rerun Model: Every Interaction Replays the Whole Script

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.

  1. Push your project to a public GitHub repo. It needs your app.py and a requirements.txt listing your libraries (for this app: streamlit, pandas).
  2. Go to share.streamlit.io, sign in with GitHub, and click “New app”.
  3. Pick the repo, branch, and the path to app.py, then click Deploy.
  4. After a minute you get a public URL like https://your-app.streamlit.app that 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_resource for one-time objects like a model or a database connection.
  • DO keep filters and controls in st.sidebar so the main area stays focused on results.
  • DO use st.form when several inputs should be submitted together, so the app reruns once instead of after every field.
  • DO commit a small, hand-written requirements.txt before 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.

Previous: Plotly: Interactive Visualizations & Dashboards

Next: Python: Matplotlib vs Seaborn vs Plotly, Visualization Libraries Compared

Series Home: Python + AI/ML Tutorial Series

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *