We use cookies to enhance your experience on the site
CodeWorlds

Streamlit - a research station in the browser

You have gathered the data, @name, you have run the statistics - but how do you show any of it to the rest of the expedition, the half that cannot read code? We are going to build an interactive research station: a page where anyone can drag a slider and watch the charts answer. Streamlit does all of that without a single line of HTML.

1pip install streamlit

One package, and Python gains a browser. From here on everything happens in a plain

.py
file - no front-end project, no build step, no template engine.

The rule that matters most: the script is the page

Remember this one idea and the rest of the lesson becomes obvious. In Streamlit you write an ordinary Python script from top to bottom, and every

st.something(...)
call draws one more element onto the page. There are no templates and no server to write - the script is the page.

1# app.py
2import streamlit as st
3import pandas as pd
4
5st.title('Safari Station')
6st.write('Interactive analysis of data from the reserve')
7
8data = pd.DataFrame({
9    'species': ['Lion', 'Elephant', 'Cheetah', 'Giraffe', 'Zebra'],
10    'population': [120, 450, 85, 200, 380],
11    'endangered': [True, False, True, False, False]
12})
13
14st.dataframe(data)

Read that code the way you would read the page it produces:

st.title
is the heading,
st.write
is the paragraph,
st.dataframe
drops in an interactive table. Order in the script equals order on the page. The import line is always the same -
import streamlit as st
- and every element you will ever add hangs off that
st
. What you do not do is launch it with
python app.py
. Streamlit ships its own command:

1streamlit run app.py

streamlit run app.py
starts a local server and opens your station in the browser. From then on, every save of the file refreshes the page - you write like a scripter and you watch an application. Nothing else will do the job:
python app.py
executes the file as a plain script and shows you nothing, while
run streamlit app.py
and
streamlit app.py run
are the same words in the wrong order.

Widgets - and the second rule

Now the mechanism that makes the whole thing tick. When the user drags a slider or picks an entry from a list, Streamlit reruns the entire script from top to bottom - and each widget hands back the value that is selected at that moment. This is why you never write event handlers here: you read a widget exactly like an ordinary variable.

1import streamlit as st
2
3st.sidebar.title('Filters')
4
5species = st.sidebar.selectbox(
6    'Select species:',
7    ['All', 'Lion', 'Elephant', 'Cheetah']
8)
9
10min_population = st.sidebar.slider(
11    'Minimum population:', min_value=0, max_value=1000, value=100
12)
13
14show_endangered = st.sidebar.checkbox('Endangered only', value=False)

Look closely:

species
is not a callback of any kind - it is simply the species chosen right now in the list. Every change restarts the script,
species
arrives with a new value, and you use it to filter the data further down. The
st.sidebar
prefix parks a widget in the side panel so the filters do not crowd the main view, which is the natural home for them. It is always
st.sidebar.selectbox()
- there is no
st.menu
, no
st.nav
and no
st.left
. The same panel happily takes
st.multiselect
,
st.radio
and
st.number_input
when one choice is not enough.

Charts - show it, do not describe it

Numbers in a table say very little; what convinces a fellow researcher is a chart. Streamlit accepts charts from the libraries you already know and puts them on the page with a single command, so nothing you learned about plotting goes to waste.

1import plotly.express as px
2
3# Interactive Plotly chart
4fig = px.bar(data, x='species', y='population', color='endangered',
5             title='Species populations')
6st.plotly_chart(fig)
7
8# Built-in Streamlit charts - the shortest route
9st.bar_chart(data.set_index('species')['population'])

You have two routes here.

st.plotly_chart
takes a finished Plotly figure and gives you full control and full interactivity, while
st.bar_chart
draws a simple chart in one call when the details do not matter. A Matplotlib figure travels the same road through
st.pyplot(fig)
. The rule of thumb: start with the built-in charts and reach for Plotly the moment you feel where they stop.

Page layout - columns and tabs

By default the elements stack one under another, in the order you wrote them. To make the station look like a dashboard instead of a very long list, you spread them across columns and hide the rest behind tabs.

1col1, col2, col3 = st.columns(3)
2with col1:
3    st.metric('Species', 150, delta=5)
4with col2:
5    st.metric('Population', '12,500', delta=-200)
6with col3:
7    st.metric('Endangered', 23, delta=2, delta_color='inverse')
8
9tab1, tab2 = st.tabs(['Data', 'Charts'])
10with tab1:
11    st.dataframe(data)
12with tab2:
13    st.bar_chart(data.set_index('species')['population'])

The

with col1:
construction means "everything in this block lands in the first column".
st.metric
shows a single number together with its trend (
delta
), which makes it perfect for the headline figures across the top of a dashboard. Tabs (
st.tabs
) tuck the charts behind the data so the user decides what to look at, and
st.expander
pulls the same trick for one block of details at a time.

Cache - do not compute the same thing twice

Recall the second rule: every interaction runs the script again. That is wonderfully convenient, but if a huge CSV file were reloaded on every twitch of the slider, the station would crawl. Caching tells Streamlit to remember what a function returned and to skip the work on the next pass.

1@st.cache_data
2def load_data():
3    return pd.read_csv('large_safari_data.csv')
4
5data = load_data()
6st.dataframe(data)

The decorator is

@st.cache_data
- that exact name, and not
@st.store
,
@st.memoize
or
@st.remember
, none of which exist in Streamlit. With it in place the file is read once, and every later rerun of the script reaches for the remembered result instead. This is the single most valuable optimization in Streamlit: put a cache on everything that is slow and does not change, such as loading data or a heavy computation.

Everything together - the complete station

Let us bring every piece into one working station. Read the code from the top and watch the standard shape of a Streamlit app emerge: imports and page configuration first, then the app title and description, then the sidebar with filters, then the main content with charts, and the data table at the very bottom.

1# safari_dashboard.py
2import streamlit as st
3import pandas as pd
4import plotly.express as px
5
6st.set_page_config(page_title='Safari Dashboard', page_icon='', layout='wide')
7
8st.title('Safari Species Dashboard')
9
10# Sidebar filters
11st.sidebar.header('Filters')
12min_pop = st.sidebar.slider('Min population', 0, 500, 50)
13
14# Load data
15@st.cache_data
16def load_data():
17    return pd.DataFrame({
18        'species': ['Lion', 'Elephant', 'Cheetah', 'Giraffe', 'Zebra'] * 3,
19        'population': [120, 450, 85, 200, 380, 130, 460, 80, 210, 390, 125, 455, 82, 205, 385],
20        'year': [2021]*5 + [2022]*5 + [2023]*5,
21        'habitat': ['Savanna', 'Forest', 'Savanna', 'Savanna', 'Savanna'] * 3
22    })
23
24data = load_data()
25filtered = data[data['population'] >= min_pop]
26
27# Metrics
28col1, col2, col3 = st.columns(3)
29col1.metric('Species', filtered['species'].nunique())
30col2.metric('Total Population', filtered['population'].sum())
31col3.metric('Average Population', f"{filtered['population'].mean():.0f}")
32
33# Charts
34col1, col2 = st.columns(2)
35
36with col1:
37    fig = px.bar(filtered, x='species', y='population', color='year',
38                 title='Populations by Species')
39    st.plotly_chart(fig, use_container_width=True)
40
41with col2:
42    fig = px.pie(filtered, values='population', names='species',
43                 title='Population Share')
44    st.plotly_chart(fig, use_container_width=True)
45
46# Data table
47st.subheader('Data')
48st.dataframe(filtered, use_container_width=True)

Trace the flow through it:

load_data
sits behind a cache, so the heavy data is read once;
min_pop
from the slider filters that data on every move of the handle, and the metrics and the charts are recomputed on the filtered set. That is the entire station - fewer than forty lines, launched with
streamlit run safari_dashboard.py
, and not one line of HTML anywhere.

Carry two rules away from this lesson and the documentation will fill in everything else: a script read from top to bottom is the page, and every interaction runs it from the start again - which makes widgets nothing more than variables holding the value selected right now.

Go to CodeWorlds