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 streamlitOne 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.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.pystreamlit 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.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.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.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.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.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.