featured image

Building Interactive Web Apps with Streamlit

A comprehensive tutorial on using Streamlit to create interactive web applications with Python. Learn how to build a medical monitoring dashboard that integrates with a Flask backend, handles file uploads, manages session state, and implements auto-refreshing data displays—all without writing any HTML, CSS, or JavaScript.

Published

Sat Jun 14 2025

Technologies Used

Python Streamlit
Intermediate 19 minutes

Purpose

The Problem

You’ve built a powerful backend API with Flask. You’ve created signal processing algorithms that convert raw sensor data into clinical metrics. But now you need a user interface, and you face a dilemma:

Option 1: Traditional Web Development

// 500+ lines of React boilerplate
import React, { useState, useEffect } from 'react';
// Set up state management
// Handle API calls with fetch/axios
// Create form components
// Implement validation
// Style with CSS
// Configure webpack/babel
// Deploy with nginx

Option 2: Desktop GUI (Tkinter)

# Works, but limited to local machines
# No mobile access
# Requires installation on every workstation
# Hard to update (must redeploy to all clients)

The Reality: Building a production-quality web interface requires 1000+ lines of HTML/CSS/JavaScript—and that’s before adding charts, real-time updates, or file uploads.

The Solution

Streamlit lets you build interactive web applications using pure Python—no HTML, CSS, or JavaScript required. The patient_streamlit.py file implements a complete medical monitoring interface in 646 lines of Python that includes:

  • Form inputs with validation
  • File uploads with real-time processing
  • Auto-refreshing data displays
  • Interactive charts with Matplotlib
  • Multi-tab navigation
  • Session state management

All deployable to any cloud platform with a single streamlit run command.

What You Will Learn

This Streamlit app demonstrates production-level patterns:

  • Session state management for multi-step workflows
  • Polling mechanisms for real-time server updates
  • Environment-based configuration for dev/prod deployments
  • Error boundary handling for graceful degradation
  • Responsive layouts with columns and containers

Prerequisites & Tooling

Knowledge Base:

  • Python basics (functions, dictionaries, loops)
  • Flask/REST API concepts (GET/POST requests)
  • Basic understanding of state management

Environment:

pip install streamlit requests matplotlib pandas
streamlit --version  # Should be 1.20+

Project Structure:

project/
├── patient_streamlit.py     # Streamlit app
├── server.py                # Flask API backend
├── cpap_measurements.py     # Signal processing
└── sample_data/
    └── patient_01.txt       # Sample CPAP files

High-Level Architecture

graph TD
    A[Browser] -->|HTTP Request| B[Streamlit Server :8501]
    B -->|Reruns Script Top-to-Bottom| C[patient_streamlit.py]
    C -->|Read| D[Session State]
    C -->|POST /add_patient| E[Flask API :5000]
    C -->|GET /CPAP_query| E
    C -->|GET /get_all_patients| E
    E -->|JSON Response| C
    C -->|Update| D
    C -->|Render| F[HTML/JS Frontend]
    F -->|Display| A

    style B fill:#FF6B6B
    style E fill:#4ECDC4
    style D fill:#FFE66D

Streamlit’s Execution Model:

sequenceDiagram
    participant User
    participant Browser
    participant Streamlit
    participant Script

    User->>Browser: Click button
    Browser->>Streamlit: Send event
    Streamlit->>Script: Re-execute entire script
    Script->>Script: Load session state
    Script->>Script: Process button click
    Script->>Script: Render UI components
    Streamlit->>Browser: Send updated HTML
    Browser->>User: Display new state

Analogy: Think of Streamlit like a video game render loop:

  • Traditional web app: Complex state machine with event handlers (like programming AI for each NPC)
  • Streamlit: Simple top-to-bottom script that reruns on every interaction (like redrawing the entire screen 60 times per second)

Streamlit optimizes this by only re-rendering changed components and caching expensive operations.

The Implementation

Step 1: Basic App Structure and Configuration

Logic: Every Streamlit app starts with configuration and imports. The script runs top-to-bottom on every user interaction.

import streamlit as st
import requests
import os
from cpap_measurements import analysis_driver

# Server configuration - use environment variable or default
SERVER = os.environ.get("API_URL", "http://127.0.0.1:5000")

# Page configuration (must be first Streamlit command)
st.set_page_config(
    page_title="Sleep Lab Patient Monitoring System",
    page_icon="🏥",
    layout="wide",              # Use full browser width
    initial_sidebar_state="expanded"  # Sidebar visible by default
)

🔵 Deep Dive: Why environment variables for configuration?

Problem: Hardcoded URLs break when deploying:

# ❌ BAD: Works locally, fails in production
SERVER = "http://127.0.0.1:5000"

# In Docker container, Flask runs on different host
# In cloud, API might be at https://api.example.com

Solution: Environment-based config:

# ✅ GOOD: Works everywhere
SERVER = os.environ.get("API_URL", "http://127.0.0.1:5000")

# Local development: Uses default
# Docker: Set API_URL=http://api:5000
# Production: Set API_URL=https://api.yourcompany.com

Deployment examples:

# Local development
streamlit run patient_streamlit.py
# Uses default: http://127.0.0.1:5000

# Docker
docker run -e API_URL=http://api:5000 app
# Uses: http://api:5000

# Heroku/Cloud
heroku config:set API_URL=https://myapi.herokuapp.com
# Uses: https://myapi.herokuapp.com

Step 2: Session State - Managing State Across Reruns

Logic: Streamlit reruns your entire script on every interaction. Session state persists data between reruns.

# Initialize session state (runs once)
if 'patient_locked' not in st.session_state:
    st.session_state.patient_locked = False
if 'cpap_data' not in st.session_state:
    st.session_state.cpap_data = None
if 'last_poll_time' not in st.session_state:
    st.session_state.last_poll_time = 0
if 'mrn' not in st.session_state:
    st.session_state.mrn = ""
if 'room_number' not in st.session_state:
    st.session_state.room_number = ""

🔴 Danger: Understanding when session state initializes

Common mistake:

# ❌ WRONG: Resets on every rerun
st.session_state.counter = 0

if st.button("Increment"):
    st.session_state.counter += 1

st.write(f"Counter: {st.session_state.counter}")
# Always shows 0 because line 2 resets it!

Correct approach:

# ✅ CORRECT: Only initialize if not exists
if 'counter' not in st.session_state:
    st.session_state.counter = 0

if st.button("Increment"):
    st.session_state.counter += 1

st.write(f"Counter: {st.session_state.counter}")
# Shows: 0, 1, 2, 3, ...

How it works:

# Rerun 1:
if 'counter' not in st.session_state:  # True (doesn't exist)
    st.session_state.counter = 0       # Set to 0
# Button clicked, counter becomes 1

# Rerun 2:
if 'counter' not in st.session_state:  # False (exists with value 1)
    st.session_state.counter = 0       # SKIPPED
# Button clicked, counter becomes 2

Step 3: Form Inputs with State Binding

Logic: Bind input widgets to session state so values persist across reruns.

def main():
    st.title("🏥 Sleep Lab Patient Monitoring System")

    # Create two-column layout
    col1, col2 = st.columns(2)

    with col1:
        # Text input bound to session state
        patient_name = st.text_input(
            "Patient Name",
            value=st.session_state.patient_name,  # Load from state
            key="name_input"
        )
        st.session_state.patient_name = patient_name  # Save to state

        mrn = st.text_input(
            "Medical Record Number (MRN)*",
            value=st.session_state.mrn,
            disabled=st.session_state.patient_locked,  # Lock after first upload
            key="mrn_input",
            help="Required field"
        )
        if not st.session_state.patient_locked:
            st.session_state.mrn = mrn

    with col2:
        room_number = st.text_input(
            "Room Number*",
            value=st.session_state.room_number,
            disabled=st.session_state.patient_locked,
            key="room_input",
            help="Required field"
        )
        if not st.session_state.patient_locked:
            st.session_state.room_number = room_number

        cpap_pressure = st.text_input(
            "CPAP Pressure (cmH2O)",
            value=st.session_state.cpap_pressure,
            key="pressure_input",
            help="Must be an integer between 4 and 25"
        )
        st.session_state.cpap_pressure = cpap_pressure

🔵 Deep Dive: Widget keys and automatic state binding

Two ways to bind widgets to state:

Method 1: Manual binding (shown above)

# Explicit: Read from state, update state
value = st.text_input("Name", value=st.session_state.name)
st.session_state.name = value

Method 2: Automatic with key

# Automatic: Streamlit handles it
st.text_input("Name", key="name")
# Access with: st.session_state.name
# No manual assignment needed!

When to use each:

# Use manual when you need conditional updates
if not st.session_state.locked:
    st.session_state.mrn = st.text_input("MRN", value=st.session_state.mrn)
# Prevents updates when locked

# Use automatic for simple cases
st.text_input("Name", key="name")
st.write(f"Hello, {st.session_state.name}")

Step 4: File Upload with Processing

Logic: Handle file uploads, process them with your existing Python code, and display results.

def process_cpap_file(uploaded_file):
    """
    Processes uploaded CPAP data file

    Parameters
    ----------
    uploaded_file : UploadedFile
        Streamlit uploaded file object

    Returns
    -------
    dict or None
        Dictionary with breath_rate, apnea_count, time, flow, image_b64
    """
    import os
    import time

    temp_path = None
    try:
        # Save uploaded file temporarily
        temp_path = f"temp_cpap_{int(time.time())}.csv"
        with open(temp_path, "wb") as f:
            f.write(uploaded_file.getbuffer())

        # Analyze the file using existing signal processing
        breath_rate, apnea_count, time_data, flow_data = analysis_driver(temp_path)

        # Create plot
        from matplotlib.figure import Figure
        fig = Figure(figsize=(12, 5))
        ax = fig.add_subplot(111)
        ax.plot(time_data, flow_data, color='#3498db', linewidth=1.5)
        ax.set_xlabel('Time (seconds)', fontsize=11, fontweight='bold')
        ax.set_ylabel('Flow (cubic meters per second)', fontsize=11, fontweight='bold')
        ax.set_title('CPAP Flow Rate Analysis', fontsize=12, fontweight='bold')
        ax.grid(True, alpha=0.3)

        # Convert to base64 for API
        image_b64 = plot_to_b64(fig)

        # Clean up temp file
        if temp_path and os.path.exists(temp_path):
            os.remove(temp_path)

        return {
            'breath_rate': breath_rate,
            'apnea_count': apnea_count,
            'time': time_data,
            'flow': flow_data,
            'image_b64': image_b64,
            'fig': fig
        }
    except Exception as e:
        # Clean up temp file on error
        if temp_path and os.path.exists(temp_path):
            try:
                os.remove(temp_path)
            except:
                pass
        st.error(f"Error processing CPAP file: {str(e)}")
        return None


# In main app:
uploaded_file = st.file_uploader(
    "Upload CPAP Data File",
    type=['csv', 'txt'],
    help="Select a CPAP data file for analysis",
    key="cpap_file_uploader"
)

if uploaded_file is not None:
    with st.spinner("Processing CPAP data..."):
        cpap_data = process_cpap_file(uploaded_file)
        if cpap_data:
            st.session_state.cpap_data = cpap_data
            st.success(f"✅ File processed: {uploaded_file.name}")
            st.rerun()  # Refresh to show results

🔴 Danger: File uploads trigger multiple reruns

Problem: Streamlit reruns script when file is uploaded, leading to duplicate processing:

# ❌ BAD: Processes file on every rerun
uploaded_file = st.file_uploader("Upload file")
if uploaded_file:
    data = expensive_processing(uploaded_file)  # Runs repeatedly!

Solution: Track processed files

# ✅ GOOD: Process only once per file
if 'last_processed_file' not in st.session_state:
    st.session_state.last_processed_file = None

uploaded_file = st.file_uploader("Upload file")
if uploaded_file:
    file_id = uploaded_file.name + str(uploaded_file.size)

    if st.session_state.last_processed_file != file_id:
        with st.spinner("Processing..."):
            data = expensive_processing(uploaded_file)
            st.session_state.cpap_data = data
            st.session_state.last_processed_file = file_id
            st.success("File processed!")
    else:
        st.info(f"Already loaded: {uploaded_file.name}")

Step 5: Auto-Refresh with Polling

Logic: Implement auto-refresh to poll the server for updates without WebSockets.

with st.sidebar:
    st.header("⚙️ Settings")

    # Auto-refresh toggle
    auto_refresh = st.checkbox("Auto-refresh CPAP Pressure", value=True)
    if auto_refresh:
        refresh_interval = st.slider("Refresh interval (seconds)", 10, 60, 30)

        # Check if it's time to poll
        import time
        current_time = time.time()
        if current_time - st.session_state.last_poll_time > refresh_interval:
            if st.session_state.room_number:
                new_pressure = query_server_for_pressure(st.session_state.room_number)
                if new_pressure and new_pressure != st.session_state.cpap_pressure:
                    st.session_state.cpap_pressure = new_pressure
                    st.sidebar.success(f"🔄 Updated pressure: {new_pressure} cmH2O")
            st.session_state.last_poll_time = current_time
            time.sleep(0.1)
            st.rerun()  # Trigger rerun to fetch new data

🔵 Deep Dive: How auto-refresh works

Streamlit’s execution model:

  1. User opens page
  2. Script runs top-to-bottom
  3. Script pauses, waits for user interaction
  4. User clicks button → script reruns
  5. Repeat

Problem: No automatic reruns for polling.

Solution: Manual rerun with st.rerun():

# Check if enough time passed
if time.time() - last_check > INTERVAL:
    new_data = fetch_from_server()
    st.session_state.data = new_data
    st.rerun()  # Force rerun without user interaction

Flow:

1. Script runs → Check timer → 10 seconds haven't passed → Stop
2. (10 seconds later, script still paused)
3. Script runs → Check timer → 10 seconds passed → Poll server → st.rerun()
4. Script runs again (showing new data) → Check timer → Reset timer → Stop
5. (Wait another 10 seconds...)

Performance consideration:

# ❌ BAD: Busy loop (100% CPU)
while True:
    if time.time() - last > interval:
        poll_server()
        st.rerun()

# ✅ GOOD: Check once per script run
if time.time() - last > interval:
    poll_server()
    st.rerun()
# Script pauses until next interaction

Step 6: Multi-Tab Layout

Logic: Organize complex UIs with tabs for different sections.

# Create tabs
tab1, tab2 = st.tabs(["📋 Patient Data Entry", "👥 View All Patients"])

with tab1:
    st.header("Patient Information")
    # ... patient entry form

with tab2:
    st.header("All Patients Database View")

    if st.button("🔄 Refresh Patient List"):
        st.rerun()

    with st.spinner("Loading patients..."):
        success, data = get_all_patients()

    if success:
        patients = data

        for patient in patients:
            with st.expander(
                f"🏥 Room {patient['room_number']} - {patient['patient_name']}",
                expanded=False
            ):
                # Display patient details
                if len(patient.get('CPAP_pressure', [])) > 0:
                    # Create dataframe for display
                    import pandas as pd
                    records = []
                    for i in range(len(patient['CPAP_pressure'])):
                        record = {
                            "Timestamp": patient['timestamp'][i],
                            "CPAP Pressure": patient['CPAP_pressure'][i],
                            "Breath Rate": patient['breath_rate'][i],
                            "Apnea Events": patient['apnea_count'][i]
                        }
                        records.append(record)

                    df = pd.DataFrame(records)

                    # Highlight rows with high apnea counts
                    def highlight_apnea(row):
                        if row['Apnea Events'] >= 2:
                            return ['background-color: #ffe6e6'] * len(row)
                        return [''] * len(row)

                    styled_df = df.style.apply(highlight_apnea, axis=1)
                    st.dataframe(styled_df, use_container_width=True)

Streamlit layout components:

# Tabs
tab1, tab2, tab3 = st.tabs(["Tab 1", "Tab 2", "Tab 3"])

# Columns
col1, col2, col3 = st.columns([2, 1, 1])  # Ratio 2:1:1

# Expander
with st.expander("Click to expand"):
    st.write("Hidden content")

# Sidebar
with st.sidebar:
    st.write("Sidebar content")

# Container
with st.container():
    st.write("Grouped content")

Step 7: Custom CSS Styling

Logic: Override default Streamlit styling with custom CSS.

st.markdown("""
    <style>
    .main {
        padding: 0rem 1rem;
    }
    .stAlert {
        margin-top: 1rem;
    }
    h1 {
        color: #2c3e50;
        padding: 1rem 0;
    }
    h2 {
        color: #3498db;
        padding: 0.5rem 0;
    }
    .metric-card {
        background-color: #f0f4f8;
        padding: 1rem;
        border-radius: 0.5rem;
        border: 2px solid #3498db;
    }
    .alert-metric {
        background-color: #ffe6e6;
        border-color: #e74c3c;
    }
    </style>
""", unsafe_allow_html=True)

# Use custom classes
st.markdown(
    f"""<div class="metric-card alert-metric">
    <h3 style="color: #e74c3c;">⚠️ Apnea Events</h3>
    <h2 style="color: #e74c3c;">{apnea_count}</h2>
    </div>""",
    unsafe_allow_html=True
)

🔴 Danger: unsafe_allow_html=True security risk

Problem: Allows XSS (Cross-Site Scripting) attacks if user input is unsanitized:

# ❌ DANGEROUS: User input injected into HTML
user_name = st.text_input("Name")
st.markdown(f"<h1>Welcome {user_name}</h1>", unsafe_allow_html=True)

# User enters: <script>alert('Hacked!')</script>
# Result: JavaScript executes in browser

Safe approach:

# ✅ SAFE: Use Streamlit's built-in components
st.header(f"Welcome {user_name}")  # Auto-escapes HTML

# ✅ SAFE: Sanitize user input
import html
safe_name = html.escape(user_name)
st.markdown(f"<h1>Welcome {safe_name}</h1>", unsafe_allow_html=True)

Step 8: API Integration

Logic: Make HTTP requests to your Flask backend from Streamlit.

def upload_to_server(mrn, room_number, patient_name, cpap_pressure, cpap_data):
    """
    Uploads patient data to Flask API

    Returns
    -------
    tuple
        (success: bool, message: string)
    """
    # Validate required fields
    if not mrn:
        return False, "Missing Patient Medical Record Number"
    if not room_number:
        return False, "Missing Room Number"

    # Prepare data
    if cpap_data:
        breath_rate = cpap_data['breath_rate']
        apnea_count = cpap_data['apnea_count']
        image = cpap_data['image_b64']
    else:
        breath_rate = "Not measured"
        apnea_count = "Not measured"
        image = ""

    # Create JSON payload
    patient = {
        "patient_name": patient_name,
        "patient_mrn": int(mrn),
        "room_number": int(room_number),
        "CPAP_pressure": cpap_pressure,
        "breath_rate": breath_rate,
        "apnea_count": apnea_count,
        "flow_image": image
    }

    try:
        # POST to Flask API
        r = requests.post(f"{SERVER}/add_patient", json=patient, timeout=10)
        if r.status_code == 200:
            return True, r.text
        else:
            return False, f"Server error: {r.text}"
    except Exception as e:
        return False, f"Connection error: {str(e)}"


# In main app:
if st.button("📤 Upload to Server", type="primary"):
    with st.spinner("Uploading data..."):
        success, message = upload_to_server(
            st.session_state.mrn,
            st.session_state.room_number,
            st.session_state.patient_name,
            st.session_state.cpap_pressure,
            st.session_state.cpap_data
        )

        if success:
            st.success(f"✅ {message}")
            st.session_state.patient_locked = True
        else:
            st.error(f"❌ {message}")

Under the Hood

Streamlit’s Architecture

Client-Server model:

Browser (JavaScript) ←→ Streamlit Server (Python) ←→ Your Script
       WebSocket              Script Reruns           Business Logic

What happens on user interaction:

  1. User clicks button:

    // Browser sends WebSocket message
    {type: "click", widgetId: "upload_button"}
  2. Streamlit server receives event:

    # Server queues script rerun
    rerun_script(script_path="patient_streamlit.py")
  3. Script executes top-to-bottom:

    # Every line runs again
    st.title("...")  # Renders title
    if st.button("Upload"):  # This time returns True
        # Button handler code runs
  4. Streamlit tracks widget changes:

    # Internal diff algorithm
    old_widgets = [Title("..."), Button("Upload")]
    new_widgets = [Title("..."), Button("Upload")]
    # Only changed widgets are re-rendered in browser
  5. Browser updates DOM:

    // Receives delta updates via WebSocket
    updateElement("upload_button", {clicked: true})

Session State Implementation

Under the hood, session state is:

# Simplified implementation
class SessionState:
    def __init__(self):
        self._state = {}  # Dictionary per session

    def __getattr__(self, key):
        return self._state.get(key)

    def __setattr__(self, key, value):
        if key == '_state':
            super().__setattr__(key, value)
        else:
            self._state[key] = value

    def __contains__(self, key):
        return key in self._state

# Streamlit maintains one SessionState per browser session
sessions = {}  # session_id -> SessionState

Session identification:

# Browser cookie stores session ID
session_id = "abc123..."

# Server looks up session
st.session_state = sessions[session_id]

Memory usage:

# Each session stores Python objects in RAM
sessions = {
    "user1": {"counter": 5, "data": [...1000 items...]},
    "user2": {"counter": 3, "data": [...1000 items...]},
    # ...
}
# For 100 concurrent users with 1MB data each: 100MB RAM

Caching for Performance

Problem: Expensive operations run on every rerun:

# ❌ SLOW: Loads CSV on every button click
@st.cache_data  # Decorator caches results
def load_data(file_path):
    return pd.read_csv(file_path)  # Only runs once

data = load_data("large_file.csv")
st.write(data)

How caching works:

# Simplified implementation
cache = {}

def cache_data(func):
    def wrapper(*args, **kwargs):
        # Create cache key from function + arguments
        key = (func.__name__, args, tuple(kwargs.items()))

        if key in cache:
            return cache[key]  # Cache hit

        result = func(*args, **kwargs)  # Cache miss
        cache[key] = result
        return result
    return wrapper

Cache types:

@st.cache_data  # For data (DataFrames, lists, dicts)
def load_csv(path):
    return pd.read_csv(path)

@st.cache_resource  # For resources (DB connections, ML models)
def get_database():
    return connect_to_db()

# Without cache: Runs every rerun
# With cache: Runs once, returns cached result

Edge Cases & Pitfalls

Edge Case 1: Widget Keys Must Be Unique

Problem: Duplicate keys cause unexpected behavior:

# ❌ BAD: Same key in different tabs
with tab1:
    st.text_input("Name", key="name")

with tab2:
    st.text_input("Name", key="name")  # Duplicate key!

# Error: DuplicateWidgetID: There are multiple widgets with key='name'

Solution: Namespace keys:

# ✅ GOOD: Unique keys
with tab1:
    st.text_input("Name", key="tab1_name")

with tab2:
    st.text_input("Name", key="tab2_name")

Edge Case 2: Session State Shared Across Tabs

Scenario: User opens app in multiple browser tabs.

# Tab 1:
st.session_state.counter = 0

# Tab 2:
st.session_state.counter = 5

# Tab 1 refreshes:
# Expects: 0
# Gets: 5 (session is shared!)

Each browser tab gets separate session:

  • Different tabs = Different session IDs
  • Incognito window = New session
  • Different users = Different sessions

Shared data requires external storage:

# ❌ BAD: Won't sync across tabs
st.session_state.shared_data = [...]

# ✅ GOOD: Store in database or Redis
def get_shared_data():
    return redis_client.get("shared_data")

Edge Case 3: File Upload Size Limits

Default limit: 200MB

# Configure in .streamlit/config.toml
[server]
maxUploadSize = 500  # 500MB

# Or set environment variable
export STREAMLIT_SERVER_MAX_UPLOAD_SIZE=500

Handling large files:

uploaded_file = st.file_uploader("Upload CPAP data", type=['csv'])

if uploaded_file:
    # Check size before processing
    file_size_mb = uploaded_file.size / (1024 * 1024)

    if file_size_mb > 100:
        st.error(f"File too large: {file_size_mb:.1f}MB. Max: 100MB")
    else:
        with st.spinner(f"Processing {file_size_mb:.1f}MB file..."):
            data = process_file(uploaded_file)

Security: Authentication

Problem: Streamlit has no built-in authentication.

Solutions:

  1. Simple password (development only):
if 'authenticated' not in st.session_state:
    st.session_state.authenticated = False

if not st.session_state.authenticated:
    password = st.text_input("Password", type="password")
    if st.button("Login"):
        if password == "secret123":  # Don't do this in production!
            st.session_state.authenticated = True
            st.rerun()
        else:
            st.error("Wrong password")
    st.stop()

# Rest of app only runs if authenticated
st.write("Welcome to secure area!")
  1. OAuth with streamlit-authenticator:
import streamlit_authenticator as stauth

authenticator = stauth.Authenticate(
    credentials,
    cookie_name,
    key,
    cookie_expiry_days
)

name, authentication_status, username = authenticator.login('Login', 'main')

if authentication_status:
    st.write(f'Welcome *{name}*')
elif authentication_status == False:
    st.error('Username/password is incorrect')
  1. Reverse proxy with nginx:
location / {
    auth_basic "Restricted";
    auth_basic_user_file /etc/nginx/.htpasswd;
    proxy_pass http://localhost:8501;
}

Conclusion

What You Learned:

  1. Streamlit Execution Model: Script reruns top-to-bottom on every interaction
  2. Session State: Persist data across reruns with st.session_state
  3. Widget Binding: Connect form inputs to state with keys and values
  4. File Upload: Handle file uploads and integrate with existing processing code
  5. Auto-Refresh: Implement polling without WebSockets using st.rerun()
  6. Layout Components: Organize UIs with tabs, columns, expanders
  7. API Integration: Connect Streamlit frontend to Flask backend

Advanced Concepts Demonstrated:

  • WebSocket architecture between browser and Streamlit server
  • Diff algorithm for efficient DOM updates
  • Session management with browser cookies
  • Caching strategies for performance optimization
  • Environment-based configuration for deployment flexibility

Skill Transfer: These patterns apply to:

  • Data science dashboards (visualize ML models, datasets)
  • Internal tools (admin panels, monitoring dashboards)
  • Prototypes (quickly demo ideas to stakeholders)
  • Reports (interactive reports with filters and drill-downs)
  • IoT monitoring (real-time sensor data visualization)

When to Use Streamlit vs. Traditional Web:

Use Streamlit when:

  • Building internal tools for technical users
  • Rapid prototyping (hours instead of weeks)
  • Data-heavy applications (charts, tables, metrics)
  • Python-first teams (no front-end developers)

Use React/Flask when:

  • Public-facing applications
  • Complex user interactions (drag-and-drop, real-time collaboration)
  • Fine-grained UI control needed
  • Mobile app required

Next Steps:

  1. Add authentication with streamlit-authenticator
  2. Deploy to Streamlit Cloud (free tier available)
  3. Implement caching with @st.cache_data for database queries
  4. Add real-time updates with st.experimental_fragment
  5. Create custom components with React (advanced)

We respect your privacy.

← View All Tutorials

Related Projects

    Ask me anything!