On this page
- Purpose
- The Problem
- The Solution
- What You Will Learn
- Prerequisites & Tooling
- High-Level Architecture
- The Implementation
- Step 1: Basic App Structure and Configuration
- Step 2: Session State - Managing State Across Reruns
- Step 3: Form Inputs with State Binding
- Step 4: File Upload with Processing
- Step 5: Auto-Refresh with Polling
- Step 6: Multi-Tab Layout
- Step 7: Custom CSS Styling
- Step 8: API Integration
- Under the Hood
- Streamlit’s Architecture
- Session State Implementation
- Caching for Performance
- Edge Cases & Pitfalls
- Edge Case 1: Widget Keys Must Be Unique
- Edge Case 2: Session State Shared Across Tabs
- Edge Case 3: File Upload Size Limits
- Security: Authentication
- Conclusion
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:
- User opens page
- Script runs top-to-bottom
- Script pauses, waits for user interaction
- User clicks button → script reruns
- 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:
-
User clicks button:
// Browser sends WebSocket message {type: "click", widgetId: "upload_button"} -
Streamlit server receives event:
# Server queues script rerun rerun_script(script_path="patient_streamlit.py") -
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 -
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 -
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:
- 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!")
- 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')
- Reverse proxy with nginx:
location / {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://localhost:8501;
}
Conclusion
What You Learned:
- Streamlit Execution Model: Script reruns top-to-bottom on every interaction
- Session State: Persist data across reruns with
st.session_state - Widget Binding: Connect form inputs to state with keys and values
- File Upload: Handle file uploads and integrate with existing processing code
- Auto-Refresh: Implement polling without WebSockets using
st.rerun() - Layout Components: Organize UIs with tabs, columns, expanders
- 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:
- Add authentication with
streamlit-authenticator - Deploy to Streamlit Cloud (free tier available)
- Implement caching with
@st.cache_datafor database queries - Add real-time updates with
st.experimental_fragment - Create custom components with React (advanced)