Creating New Streamlit Apps¶
A guide based on our existing app patterns and best practices
Project Structure¶
Based on our existing apps, use this standard structure:
your-app/
├── Streamlit/
│ ├── app_name.py # Main entry point
│ ├── pages/ # Multi-page app structure
│ │ ├── 1_📤_Upload.py # Numbered pages with emoji icons
│ │ ├── 2_📊_Analysis.py
│ │ └── 3_⚙️_Settings.py
│ ├── utils/ # Shared utilities
│ │ ├── __init__.py
│ │ ├── common.py # Common functions and constants
│ │ └── storage.py # Data handling utilities
│ ├── requirements.txt
│ ├── railway.toml # Railway deployment config
│ └── Dockerfile # Container configuration
├── data/ # Local data directory
│ ├── raw/
│ ├── processed/
│ └── parquet/
└── README.md
Step 1: Main App Entry Point¶
Create your main app file (your_app.py):
"""
Your App Name - Brief Description
Main entry point for the multipage Streamlit application.
Production-ready system for [your specific use case].
🏠 Home Page - Overview and getting started
"""
import streamlit as st
import sys
from pathlib import Path
# Add utils to path
sys.path.append(str(Path(__file__).parent))
from utils.common import get_dark_theme_css, DATA_DIR
# Page configuration
st.set_page_config(
page_title="Your App - Home",
page_icon="🏠",
layout="wide",
initial_sidebar_state="expanded"
)
# Apply consistent styling
st.markdown(get_dark_theme_css(), unsafe_allow_html=True)
def main():
"""Main home page content"""
# Main header
st.markdown("""
<div class="main-header">
<h1>🏠 Your App Name</h1>
<h3>Brief Description</h3>
<p>Detailed description of what your app does</p>
</div>
""", unsafe_allow_html=True)
# Sidebar with quick stats and navigation
with st.sidebar:
st.title("📊 Quick Stats")
# Add relevant metrics here
st.metric("📁 Total Items", "0")
st.markdown("---")
st.markdown("**🚀 Quick Start**")
st.info("👆 Use the page navigation above to get started")
# Main content layout
col1, col2 = st.columns([2, 1])
with col1:
st.subheader("🎯 What does this app do?")
st.markdown("""
Brief explanation of your app's purpose and main features.
**Key Capabilities:**
- 📤 **Feature 1**: Description
- 📊 **Feature 2**: Description
- ⚙️ **Feature 3**: Description
""")
# Feature cards
st.subheader("✨ Key Features")
features = [
{
"title": "📤 Feature 1",
"description": "Detailed description of first feature",
"page": "1_📤_Feature1"
},
{
"title": "📊 Feature 2",
"description": "Detailed description of second feature",
"page": "2_📊_Feature2"
}
]
for feature in features:
st.markdown(f"""
<div class="feature-card">
<h4>{feature['title']}</h4>
<p>{feature['description']}</p>
</div>
""", unsafe_allow_html=True)
with col2:
st.subheader("📋 How to Use")
st.markdown("""
### 1. Step One
Description of first step
### 2. Step Two
Description of second step
### 3. Step Three
Description of third step
""")
if __name__ == "__main__":
main()
Step 2: Create Utils Module¶
Create utils/common.py for shared functionality:
"""
Common Utilities
Shared utilities and constants for your Streamlit app.
"""
import streamlit as st
import pandas as pd
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional
import json
# App constants - centralized for consistency
DATA_DIR = Path(__file__).parent.parent.parent / "data"
RAW_DIR = DATA_DIR / "raw"
PROCESSED_DIR = DATA_DIR / "processed"
PARQUET_DIR = DATA_DIR / "parquet"
# Ensure directories exist
for directory in [RAW_DIR, PROCESSED_DIR, PARQUET_DIR]:
directory.mkdir(parents=True, exist_ok=True)
def get_dark_theme_css() -> str:
"""Common dark theme CSS used across all pages"""
return """
<style>
/* Dark theme detection and adaptive styling */
.main-header {
background: linear-gradient(90deg, #1f4e79, #2c5f8a);
color: white;
padding: 1.5rem;
border-radius: 10px;
margin-bottom: 2rem;
text-align: center;
}
.feature-card {
background-color: var(--background-color-secondary, #f8f9fa);
padding: 1.5rem;
border-radius: 10px;
border-left: 4px solid #1f4e79;
margin-bottom: 1rem;
color: var(--text-color, inherit);
border: 1px solid var(--border-color, rgba(49, 51, 63, 0.2));
}
/* Dark theme overrides */
@media (prefers-color-scheme: dark) {
.feature-card {
background-color: rgba(49, 51, 63, 0.3) !important;
color: white !important;
border: 1px solid rgba(250, 250, 250, 0.2) !important;
}
}
/* Streamlit dark theme class detection */
[data-theme="dark"] .feature-card,
.stApp[data-theme="dark"] .feature-card {
background-color: rgba(49, 51, 63, 0.3) !important;
color: white !important;
border: 1px solid rgba(250, 250, 250, 0.2) !important;
}
.metric-highlight {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1rem;
border-radius: 10px;
text-align: center;
margin: 1rem 0;
}
.status-success {
background-color: rgba(40, 167, 69, 0.1);
border: 1px solid #28a745;
color: #28a745;
padding: 0.5rem;
border-radius: 5px;
}
.status-warning {
background-color: rgba(255, 193, 7, 0.1);
border: 1px solid #ffc107;
color: #856404;
padding: 0.5rem;
border-radius: 5px;
}
.status-error {
background-color: rgba(220, 53, 69, 0.1);
border: 1px solid #dc3545;
color: #dc3545;
padding: 0.5rem;
border-radius: 5px;
}
</style>
"""
def show_page_header(title: str, description: str, icon: str = "🏠"):
"""Standard page header used across all pages"""
st.markdown(f"""
<div class="main-header">
<h1>{icon} {title}</h1>
<p>{description}</p>
</div>
""", unsafe_allow_html=True)
def show_status_card(title: str, status: str, description: str = ""):
"""Reusable status card component"""
status_class = f"status-{status.lower()}"
st.markdown(f"""
<div class="{status_class}">
<h4>{title}</h4>
{f"<p>{description}</p>" if description else ""}
</div>
""", unsafe_allow_html=True)
@st.cache_data(ttl=300)
def load_data_file(file_path: Path) -> pd.DataFrame:
"""Cache data loading with 5-minute TTL"""
try:
if file_path.suffix == '.parquet':
return pd.read_parquet(file_path)
elif file_path.suffix == '.csv':
return pd.read_csv(file_path)
else:
st.error(f"Unsupported file type: {file_path.suffix}")
return pd.DataFrame()
except Exception as e:
st.error(f"Error loading {file_path.name}: {e}")
return pd.DataFrame()
def save_data_with_timestamp(df: pd.DataFrame, base_name: str, format: str = 'parquet') -> Path:
"""Save data with timestamp in filename"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
if format == 'parquet':
file_path = PARQUET_DIR / f"{base_name}_{timestamp}.parquet"
df.to_parquet(file_path)
else:
file_path = PROCESSED_DIR / f"{base_name}_{timestamp}.csv"
df.to_csv(file_path, index=False)
return file_path
def format_number(value: float, format_type: str = "currency") -> str:
"""Format numbers consistently across the app"""
if pd.isna(value):
return "N/A"
if format_type == "currency":
return f"${value:,.2f}"
elif format_type == "percentage":
return f"{value:.2%}"
else:
return f"{value:,.2f}"
Step 3: Create Page Templates¶
Create pages using this template (pages/1_📤_Example_Page.py):
"""
Example Page - Description of page functionality
"""
import streamlit as st
import pandas as pd
from pathlib import Path
import sys
# Add utils to path
sys.path.append(str(Path(__file__).parent.parent))
from utils.common import get_dark_theme_css, show_page_header, DATA_DIR
# Page configuration
st.set_page_config(
page_title="Your App - Example Page",
page_icon="📤",
layout="wide"
)
# Apply styling
st.markdown(get_dark_theme_css(), unsafe_allow_html=True)
def main():
"""Main page content"""
show_page_header("Example Page", "Description of what this page does", "📤")
# Sidebar
with st.sidebar:
st.title("⚙️ Settings")
# Add page-specific settings here
st.markdown("---")
st.info("💡 **Tip**: Page-specific help text here")
# Main content
st.subheader("🎯 Main Functionality")
# Example form
with st.form("example_form"):
col1, col2 = st.columns(2)
with col1:
input1 = st.text_input("Input 1", help="Description of input")
with col2:
input2 = st.selectbox("Input 2", ["Option A", "Option B"])
submitted = st.form_submit_button("Process", use_container_width=True)
if submitted:
if input1 and input2:
# Process the inputs
st.success("✅ Processing complete!")
# Display results
with st.expander("📋 Results", expanded=True):
st.write(f"Input 1: {input1}")
st.write(f"Input 2: {input2}")
else:
st.error("❌ Please fill in all required fields")
if __name__ == "__main__":
main()
Step 4: Essential Files¶
requirements.txt¶
railway.toml¶
[build]
builder = "NIXPACKS"
[deploy]
startCommand = "streamlit run your_app.py --server.port $PORT --server.address 0.0.0.0"
[[services]]
name = "your-app"
Dockerfile (optional)¶
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8501
CMD ["streamlit", "run", "your_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
Step 5: Best Practices from Our Apps¶
1. Consistent Navigation¶
- Use numbered pages with emoji icons:
1_📤_Upload.py - Keep page titles consistent: "Your App - Page Name"
- Use descriptive page icons that match functionality
2. Sidebar Structure¶
Always include in sidebar: - App status/quick stats - Page-specific settings - Help/tip sections - Navigation aids
3. Layout Patterns¶
# Standard two-column layout
col1, col2 = st.columns([2, 1])
with col1:
# Main content
pass
with col2:
# Secondary content, help, or settings
pass
4. Error Handling¶
try:
# Operation that might fail
result = risky_operation()
st.success("✅ Operation successful!")
except Exception as e:
st.error(f"❌ Error: {e}")
st.exception(e) # For debugging
5. Data Caching¶
@st.cache_data(ttl=300) # Cache for 5 minutes
def load_expensive_data():
# Expensive operation
return data
# For database connections
@st.cache_resource
def get_database_connection():
return create_connection()
6. Session State Management¶
# Initialize session state
if 'key' not in st.session_state:
st.session_state.key = default_value
# Use session state
st.session_state.key = new_value
7. Progress Indicators¶
# For long operations
progress_bar = st.progress(0)
for i, item in enumerate(items):
# Process item
progress_bar.progress((i + 1) / len(items))
progress_bar.empty() # Clean up
Step 6: Testing and Deployment¶
Local Development¶
Railway Deployment¶
- Connect your repository to Railway
- Ensure
railway.tomlis configured - Set environment variables in Railway dashboard
- Deploy with:
railway up
Additional Best Practices¶
Quick Wins (Copy These Patterns)¶
Loading States - Always show users something is happening:
Environment Check - Catch missing config early:
Input Validation - Check data before processing:
if uploaded_file is not None:
if not uploaded_file.name.endswith('.csv'):
st.error("Please upload a CSV file")
else:
# Process file
Disable Buttons During Processing - Prevent double submissions:
processing = st.session_state.get('processing', False)
if st.button("Submit", disabled=processing):
st.session_state.processing = True
# Do work...
st.session_state.processing = False
Common Patterns Summary¶
File Organization: Main app + pages/ + utils/ structure
Styling: Dark theme compatible with consistent CSS
Navigation: Numbered emoji pages with sidebar navigation
Data Handling: Cached loading with timestamp-based saving
Error Handling: Try/catch with user-friendly messages
Forms: Use st.form() for better UX
Layout: Two-column layouts with main + sidebar content
Loading States: Always show progress to users
This structure ensures consistency across all our Streamlit applications while remaining flexible for different use cases.
This guide is based on our production Streamlit apps: Warehouse, Streamlit-Testing, and Sigma-Surf-Strategy patterns.