What      Streamlit  

Can and Can't

(or Shouldn't)

Do

Hi👋

Yuichiro Tachibana

@whitphx

  • Pythonista
  • OSS enthusiast
  • ML Developer Advocate at Hugging Face
  • Streamlit Creator

Agenda

  • What's Streamlit
    • How to use it
  • What Streamlit can and can't (or shouldn't) do
    • ​Is it suitable for XX?

Streamlit is...

  • Python framework
  • to create Web UI
  • only in Python

Its target users are...

  • Any Python programmers
    who want Web UI
    but don't want to write JavaScript
  • Data scientists
  • Machine learning engineers

Hello Streamlit

$ pip install streamlit
$ code app.py

Hello Streamlit

import streamlit as st

st.title("Hello PyConFR :rainbow[2024]!")

st.markdown("This is our **first** [Streamlit](https://streamlit.io/) app :balloon:")
$ streamlit run app.py

  You can now view your Streamlit app in your browser.

  Local URL: http://localhost:8501
  Network URL: http://192.168.1.1:8501

Pure-Python Web UI framework

Write only Python, get a web app

import streamlit as st

st.title("Hello PyConFR :rainbow[2024]!")

st.markdown("This is our **first** [Streamlit](https://streamlit.io/) app :balloon:")

Display contents as you write

import streamlit as st

st.title('Awesome Streamlit app')

st.header("Data Visualizations")
st.markdown("Streamlit is a great tool to create visualizations")

st.subheader("Plotting")

st.area_chart({'data': [1, 5, 2, 6, 2, 1], 'data2': [10, 15, 12, 16, 12, 11]})

st.subheader("Maps")

st.map({"lat": [37.7749295, 35.6895, 34.052235], "lon": [-122.4194155, -139.6917, -118.243683]})

st.header("Dataframes")

st.write('Here is a simple dataframe')
st.dataframe({'A': [random.randint(0, 100) for _ in range(10)], 'B': [random.randint(0, 100) for _ in range(10)], 'C': [random.randint(0, 100) for _ in range(10)]})

st.header("Images")

st.image(image, caption='Random image from picsum.photos', use_column_width=True)

Lots of built-in components

st.line_chart()
st.table()
st.title()
st.text()
st.scatter_chart()
st.plotly_chart()
st.button()
st.selectbox()
st.date_input()
st.camera_input()
st.column()
st.chat_input()
st.navigation()
st.secret()
st.dialog()
st.fragment()
st.connection()
st.bokeh_chart()
st.area_chart()
st.bar_chart()
st.alteir_chart()
st.markdown()

Interactive UI

Interactive apps

import streamlit as st

toggle_value = st.toggle("Click me!")

if toggle_value:
    st.markdown(":large_green_circle: ON")
else:
    st.markdown(":large_red_square: OFF")

Interactive apps

import streamlit as st

toggle_value = st.toggle("Click me!")

if toggle_value:
    st.markdown(":large_green_circle: ON")
else:
    st.markdown(":large_red_square: OFF")
$ streamlit run app.py
$ streamlit run app.py

📜

app.py

Web browser

Python runtime

Streamlit

Server

Script runner

Static files

Web server

React

SPA

React SPA

JavaScript runtime

$ streamlit run app.py

📜

app.py

Web browser

Python runtime

Streamlit

Server

Script runner

Static files

Web server

React

SPA

React SPA

JavaScript runtime

User input triggers re-run

Frontend

import streamlit as st

toggle_value = st.toggle("Click me!")

if toggle_value:
    st.markdown(":large_green_circle: ON")
else:
    st.markdown(":large_red_square: OFF")

Python code

# True

Trigger re-run

Update UI

User input triggers re-run

Frontend

import streamlit as st

toggle_value = st.toggle("Click me!")

if toggle_value:
    st.markdown(":large_green_circle: ON")
else:
    st.markdown(":large_red_square: OFF")

Python code

# False

Trigger re-run

Update UI

Top-to-Bottom execution

makes it possible to build interactive UI without callbacks.

import streamlit as st

toggle_value = st.toggle("Click me!")

if toggle_value:
    st.markdown("ON")
else:
    st.markdown("OFF")
import some_framework as sf

def callback(toggle_value):
    text_field = sf.get_element(id="value")
    if toggle_value:
        text_field.text = "ON"
    else:
        text_field.text = "OFF"

sf.toggle("Click me!", callback=callback)

sf.text(id="value", text="ON")

🚨

Aside: is it like React?

  • Declarative UI
  • Colocated logic and view
import streamlit as st

toggle_state = st.toggle("Toggle me!")

if toggle_state:
    st.write("The toggle is ON!")
else:
    st.write("The toggle is OFF.")
import React, { useState } from "react";

function ToggleButton() {
  const [toggleState, setToggleState] = useState(false);

  const handleToggle = () => {
    setToggleState((prevState) => !prevState);
  };

  return (
    <div>
      <button onClick={handleToggle}>
        {toggleState ? "ON" : "OFF"}
      </button>
      <p>The toggle is {toggleState ? "ON!" : "OFF."}</p>
    </div>
  );
}

export default ToggleButton;

Imperative escape hatch

Next example: ToDo app

ToDo app code?

import streamlit as st

tasks = []

new_task_name = st.text_input("Task Name")
if new_task_name:
    tasks.append(new_task_name)

st.write("## Tasks")
for task in tasks:
    st.write(task)

🤔

Session State

import streamlit as st

# tasks = []   # ⚠️ Doesn't work.
if "tasks" not in st.session_state:
    st.session_state.tasks = []
tasks = st.session_state.tasks

new_task_name = st.text_input("Task Name")
if new_task_name:
    tasks.append(new_task_name)

st.write("## Tasks")
for task in tasks:
    st.write(task)

👍

ToDo app

State persisted over reruns

Frontend

import streamlit as st

if "tasks" not in st.session_state:
    st.session_state.tasks = []
tasks = st.session_state.tasks

new_task_name = st.text_input("Task Name")
if new_task_name:
    tasks.append(new_task_name)

st.write("## Tasks")
for task in tasks:
    st.write(task)

Python code

# []
# ["Yuichiro"]
# ["Yuichiro",
   "Alice"]

Session State

Use                                                  

for data that persists between multiple runs.

st.session_state[key]

Revisit: React

Streamlit shares some concepts with React (inspired by React?)

  • Unlike React,
    components hide the state management.
  • Like React,
    state management outside of the components is explicit.

Wrap-up: Top-to-Bottom exec

Subtitle

Pros ✅

Cons 🚨

  • Text

Layout/Design

Auto layout alignment

import streamlit as st

st.title('Awesome Streamlit app')

st.header("Data Visualizations")
st.markdown("Streamlit is a great tool to create visualizations")

st.subheader("Plotting")

st.area_chart({'data': [1, 5, 2, 6, 2, 1], 'data2': [10, 15, 12, 16, 12, 11]})

st.subheader("Maps")

st.map({"lat": [37.7749295, 35.6895, 34.052235], "lon": [-122.4194155, -139.6917, -118.243683]})

st.header("Dataframes")

st.write('Here is a simple dataframe')
st.dataframe({'A': [random.randint(0, 100) for _ in range(10)], 'B': [random.randint(0, 100) for _ in range(10)], 'C': [random.randint(0, 100) for _ in range(10)]})

st.header("Images")

st.image(image, caption='Random image from picsum.photos', use_column_width=True)

Theming

Sidebar

Main column

Unified Streamlit-ish look

Theming

Sidebar

Main column

Unified Streamlit-ish look

textColor, font
primaryColor
secondaryBackgroundColor
backgroundColor

Flexibility vs Simplicity

st.logo

You just have these options. Streamlit does the rest.

Restrictions make things simpler

You can get a nice look 'n' feel interactive app like this:

...just by writing Python code

calling                     components,

and configuring the theme if needed.

st.*()
@st.cache_data
def get_UN_data():
    AWS_BUCKET_URL = "https://streamlit-demo-data.s3-us-west-2.amazonaws.com"
    df = pd.read_csv(AWS_BUCKET_URL + "/agri.csv.gz")
    return df.set_index("Region")

try:
    df = get_UN_data()
    countries = st.multiselect(
        "Choose countries", list(df.index), ["China", "United States of America"]
    )
    if not countries:
        st.error("Please select at least one country.")
    else:
        data = df.loc[countries]
        data /= 1000000.0
        st.write("### Gross Agricultural Production ($B)", data.sort_index())

        data = data.T.reset_index()
        data = pd.melt(data, id_vars=["index"]).rename(
            columns={"index": "year", "value": "Gross Agricultural Product ($B)"}
        )
        chart = (
            alt.Chart(data)
            .mark_area(opacity=0.3)
            .encode(
                x="year:T",
                y=alt.Y("Gross Agricultural Product ($B):Q", stack=None),
                color="Region:N",
            )
        )
        st.altair_chart(chart, use_container_width=True)
except URLError as e:
    st.error(
        """
        **This demo requires internet access.**
        Connection error: %s
    """
        % e.reason
    )

You just write Python. Streamlit does the rest.

Escape hatches for custom design

Custom CSS

So why you should use Streamlit?

So why we should use Streamlit?

  • Fast and Easy development of
  • Interactive Web apps
  • Only in Python
  • Callback-less code with the top-to-bottom execution model
  • Pre-defined theming/layout

Web UI frameworks

Server-side web frameworks

Among many frameworks...

Comparison

Streamlit

(Web UI Frameworks)

Serverside Web Frameworks

+ Frontend dev

All-in-one

Pick what you need

Python only

Tech stack

Languages

Python + JS

+ Data serializer (JSON/Protobuf/...) 

Design

Low-flexibility/Easy

Full flexiblity

Logic/Modeling

Declarative

Up to you

Declarative vs Imperative

Jupyter-embeddable vs not

Flexible layout vs Easy design

Web UI frameworks

Server-side web frameworks

Comparison

Streamlit

Other Web UI Frameworks

All-in-one

All-in-one

Python only

Tech stack

Languages

Python only

Design

Low-flexibility/Easy

Per-framework

Logic/Modeling

Declarative

Per-framework

Use cases

Revisit: Todo app

import streamlit as st

if "tasks" not in st.session_state:
    st.session_state.tasks = []
tasks = st.session_state.tasks

new_task_name = st.text_input("Task Name")
if new_task_name:
    tasks.append(new_task_name)

st.write("## Tasks")
for task in tasks:
    st.write(task)

Dynamic contents

relying on data/states

  • Dynamic number of fields
  • Multi-step wizards
  • Data visualization
  • Conditional UI

Budget Allocator

Budget Allocator

Budget Allocator

Data dependencies⚡️

Budget Allocator

import streamlit as st

st.title("Budget Allocator")

# Set total budget and number of categories
total_budget = st.number_input("Total Budget ($)", min_value=1000, value=5000, step=500)
num_categories = st.number_input("Number of Categories", min_value=1, value=3)

# Generate sliders for each category
allocations = [
    st.slider(f"Category {i+1} Allocation", min_value=0, max_value=total_budget, value=total_budget // num_categories)
    for i in range(num_categories)
]

# Calculate allocated and remaining budget
total_allocated = sum(allocations)
remaining_budget = total_budget - total_allocated

# Display budget summary
st.write("### Allocation Summary")
st.write(f"Total Allocated: ${total_allocated}")
st.write(f"Remaining Budget: ${remaining_budget}")

# Provide feedback based on budget status
if total_allocated > total_budget:
    st.error("You have exceeded the total budget!")
elif remaining_budget < total_budget * 0.1:
    st.warning("Warning: You are nearing your total budget.")
else:
    st.success("You are within the budget.")
import dash
from dash import dcc, html
from dash.dependencies import Input, Output, State, ALL

# Initialize the Dash app
app = dash.Dash(__name__, suppress_callback_exceptions=True)

# App layout
app.layout = html.Div([
    html.H1("Budget Allocator"),

    # Input for total budget
    html.Label("Total Budget ($):"),
    dcc.Input(id="total-budget", type="number", value=5000, min=1000, step=500, style={"margin-bottom": "20px"}),

    # Input for number of categories
    html.Label("Number of Categories:"),
    dcc.Input(id="num-categories", type="number", value=3, min=1, style={"margin-bottom": "20px"}),

    # Placeholder for dynamically generated sliders
    html.Div(id="sliders-container", style={"margin-bottom": "20px"}),

    # Summary of allocation
    html.H3("Allocation Summary"),
    html.Div(id="summary-output"),
])

# Callback to generate sliders dynamically based on the number of categories
@app.callback(
    Output("sliders-container", "children"),
    [Input("num-categories", "value"), Input("total-budget", "value")]
)
def create_sliders(num_categories, total_budget):
    sliders = []
    for i in range(num_categories):
        slider = html.Div([
            html.Label(f"Category {i + 1} Allocation"),
            dcc.Slider(
                id={'type': 'category-slider', 'index': i},
                min=0, max=total_budget, value=total_budget // num_categories,
                marks={0: "0", total_budget: str(total_budget)}
            )
        ], style={"margin-bottom": "20px"})
        sliders.append(slider)
    return sliders

# Callback to update allocation summary based on slider values
@app.callback(
    Output("summary-output", "children"),
    Input("total-budget", "value"),
    Input({'type': 'category-slider', 'index': ALL}, 'value')
)
def update_summary(total_budget, allocations):
    total_allocated = sum(allocations)
    remaining_budget = total_budget - total_allocated

    summary = [
        html.P(f"Total Allocated: ${total_allocated}"),
        html.P(f"Remaining Budget: ${remaining_budget}")
    ]

    if total_allocated > total_budget:
        summary.append(html.Div("You have exceeded the total budget!", style={"color": "red"}))
    elif remaining_budget < total_budget * 0.1:
        summary.append(html.Div("Warning: You are nearing your total budget.", style={"color": "orange"}))
    else:
        summary.append(html.Div("You are within the budget.", style={"color": "green"}))

    return summary

# Run the app
if __name__ == "__main__":
    app.run_server(debug=True)

Example case: Dash               

  • ✅ Dynamic UI relying on data/states
  • ✅ Complex data dependencies

Data uploader and visualizer

Data uploader and visualizer

import streamlit as st
import pandas as pd

st.title("Upload and Visualize CSV Data with Streamlit")

# File uploader for CSV
uploaded_file = st.file_uploader("Choose a CSV file", type="csv")

# Display the visualization if the file is uploaded
if uploaded_file:
    data = pd.read_csv(uploaded_file)
    st.write("Data Preview:")
    st.write(data.head())

    st.line_chart(data, x="Date", y="Stock Price")
  • Reactive: Update the chart reacting the input data
  • Conditional: show the chart only when the file has been uploaded

without callbacks/imperative code!

Example case: Dash               

import dash
from dash import dcc, html
from dash.dependencies import Input, Output, State
import pandas as pd
import base64
import io

# Initialize the Dash app
app = dash.Dash(__name__)

app.layout = html.Div([
    html.H1("Upload and Visualize CSV Data with Dash"),

    # File upload component that accepts only CSV files
    dcc.Upload(
        id='upload-data',
        children=html.Button('Upload CSV'),
        accept='.csv',  # Accept only CSV files
        multiple=False
    ),

    # Placeholders for the data preview and chart
    html.Div(id='output-data-upload'),
    dcc.Graph(id='data-graph')
])

# Callback to parse the uploaded CSV and update the output
@app.callback(
    [Output('output-data-upload', 'children'), Output('data-graph', 'figure')],
    [Input('upload-data', 'contents')],
    [State('upload-data', 'filename')]
)
def update_output(contents, filename):
    if contents is None:
        return None, {}

    # Decode and parse the file
    content_type, content_string = contents.split(',')
    decoded = base64.b64decode(content_string)
    df = pd.read_csv(io.StringIO(decoded.decode('utf-8')))

    # Ensure the columns for Date and Stock Price exist
    if 'Date' not in df.columns or 'Stock Price' not in df.columns:
        return html.Div(["Error: CSV must contain 'Date' and 'Stock Price' columns."]), {}

    # Convert Date column to datetime for accurate plotting
    df['Date'] = pd.to_datetime(df['Date'])

    # Data preview
    preview = html.Div([
        html.H5(f"Uploaded File: {filename}"),
        html.P("Data Preview:"),
        html.Table([
            html.Tr([html.Th(col) for col in df.columns])] +
            [html.Tr([html.Td(df.iloc[i][col]) for col in df.columns]) for i in range(min(len(df), 5))])
    ])

    # Define the figure with specified columns
    fig = {
        'data': [{
            'x': df['Date'],
            'y': df['Stock Price'],
            'type': 'line',
            'name': 'Stock Price'
        }],
        'layout': {
            'title': 'Uploaded Data Visualization',
            'xaxis': {'title': 'Date'},
            'yaxis': {'title': 'Stock Price'}
        }
    }

    return preview, fig

# Run the app
if __name__ == '__main__':
    app.run_server(debug=True)
  • ✅ Conditional UI
  • ✅ Data visualization: No need to take care of
    • Visualization framework
    • ​Data transportation (JSON/Protobuf/...)

Dashboard

Dashboard

# Get data
# Call line_chart()
# Loop
import streamlit as st
import time
from collections import deque
from data_source import get_stock_data

st.title("Data Streaming Dashboard")

max_rows = 100
stock_data = deque(maxlen=max_rows)

chart = st.empty()

while True:
    timestamp, price = get_stock_data()

    stock_data.append((timestamp, price))

    timestamps, prices = zip(*stock_data)

    chart.line_chart({"timestamp": timestamps, "price": prices}, x="timestamp", y="price")

    time.sleep(0.01)

Large-scale dashboard

Large-scale dashboard

# For each chart,
# Get data and
# call line_chart()
# Loop
import streamlit as st
import time
from collections import deque
from data_source import get_stock_data
...
st.title(f"Data Streaming Dashboard ({NUM_ROWS}x{NUM_COLUMNS})")

tickers = [f'TICKER{i+1}' for i in range(MAX_TICKERS)]

stock_data = {ticker: deque(maxlen=MAX_ROWS) for ticker in tickers}

rows = [st.columns(NUM_COLUMNS) for _ in range(NUM_ROWS)]
charts = {}
for i, ticker in enumerate(tickers):
    row = i // NUM_COLUMNS
    col = i % NUM_COLUMNS
    charts[ticker] = rows[row][col].empty()

while True:
    for ticker in tickers:
        timestamp, price = get_stock_data(ticker)

        stock_data[ticker].append((timestamp, price))

        timestamps, prices = zip(*stock_data[ticker])

        charts[ticker].line_chart({"timestamp": timestamps, "price": prices}, x="timestamp", y="price")

    time.sleep(0.01)

Large-scale dashboard

Performance 🤔

And...

Polling vs Event-driven

✅Easy

🚨Inefficient, not precise

✅Efficient, precise

🚨Complex

while True:
    data = get_data()
    show_data(data)
def on_update(data):
    show_data(data)

data_source.subscribe(on_update)

Event-driven dashboards

Event listener👍

Queue()🤔

Can't get rid of the loop😭

import streamlit as st
from collections import deque
from queue import Queue
from event_dispatcher import data_source

st.title("Data Streaming Dashboard (Event-driven)")

max_rows = 100
stock_data = deque(maxlen=max_rows)

chart = st.empty()

q = Queue()

def on_update(timestamp, price):
    q.put((timestamp, price))

data_source.subscribe(on_update)

while True:
    timestamp, price = q.get()

    stock_data.append((timestamp, price))

    timestamps, prices = zip(*stock_data)

    chart.line_chart({"timestamp": timestamps, "price": prices}, x="timestamp", y="price")

Streamlit is not intuitive when event-driven

def on_update(new_data):
    ...
    st.line_chart(data_to_display)

data_source.subscribe(on_update)

Streamlit can't do this🚨

Revisit:

Top-to-Bottom execution

import streamlit as st
from event_dispatcher import data_source

st.title("Data Streaming Dashboard")

def on_update(new_data):
    data_to_display = ...
    st.line_chart(data_to_display)

data_source.subscribe(on_update)

The callback function is
called in another thread that is outside of Streamlit's control.

  • ✅ Live-updated UI
  • 🚨 High-frequency updates
  • 🚨 Event-driven code

More examples:

Tailor-made Frontend

Function-centric apps

e.g. AI/ML demos

def your_awesome_logic(arg1, arg2, ...):
    # your awesome logic here
    return res1, res2, ...

When you want the WebUI to use a function...

Function-centric apps

e.g. AI/ML demos

Streamlit fits perfectly,
but another might give a better abstraction.

import gradio as gr

# Core logic function
def process_data(number, text):
    transformed_number = number * 2
    uppercased_text = text.upper()
    return transformed_number, uppercased_text

# Create and launch Gradio Interface
demo = gr.Interface(
    fn=process_data,
    inputs=[
        gr.Number(label="Enter a number"),
        gr.Textbox(label="Enter some text"),
    ],
    outputs=[
        gr.Textbox(label="Transformed Number"),
        gr.Textbox(label="Uppercased Text")
    ],
    title="Data Processor",
)

demo.launch()
import streamlit as st

# Core logic function
def process_data(number, text):
    transformed_number = number * 2
    uppercased_text = text.upper()
    return transformed_number, uppercased_text

# Streamlit UI setup
st.title("Data Processor")

# Input fields
number = st.number_input("Enter a number:", value=1)
text = st.text_input("Enter some text:")

# Button to execute the function
if st.button("Process"):
    # Call the core function with user inputs
    transformed_number, uppercased_text = process_data(number, text)

    # Display the outputs
    st.write("Transformed Number:", transformed_number)
    st.write("Uppercased Text:", uppercased_text)

Function-centric apps

e.g. AI/ML demos

Streamlit fits perfectly,
but another might give a better abstraction.

Cross-user Interaction

Session
Session
Session

Isolated

Isolated

e.g. multi-user chat

💥

💥

Scaling up/out

Streamlit's server is difficult to scale up/out

Thread

Per-user WebSocket connection

→Difficult to load balancing/scale-out

No option to use multi cores

→Difficult to scale-up

Thread
Thread

High traffic apps

Subtitle

Wrap-up

Streamlit is a very good WebUI framework,
but it's not the silver bullet.

  • It simplifies the development of interactive web UI based on Python logic,
    with its unique execution model and pre-built components and themes.
  • It may not be suitable in some cases;
    large systems, event-driven data source, high traffic apps, etc.
  • Different technologies may be more appropriate for your case.
    Choose the right one based on a clear understanding of these tech stacks.

Happy Streamlit-ing!

Bonus: LLM-friendliness

Streamlit

  • has stable API
  • has large amount of user code

What Streamlit can and can't (or shouldn't) do

By whitphx

What Streamlit can and can't (or shouldn't) do

  • 140