| name | streamlit-uv-development |
| description | Use this skill when developing Streamlit applications with uv package manager. Covers project setup, running apps, testing (unit/e2e), and development workflows. Trigger when user mentions "streamlit", "uv run streamlit", "streamlit testing", or asks about building data apps with Python. |
Streamlit Development with uv
Overview
This skill enables rapid Streamlit application development using uv as the package manager. It covers project initialization, running apps, testing strategies, and production-ready development patterns.
Quick Start
Create New Streamlit Project
# Create project directory
mkdir my-streamlit-app && cd my-streamlit-app
# Initialize with uv
uv init --name my-streamlit-app
# Add streamlit dependency
uv add streamlit
# Create main app file
cat > app.py << 'EOF'
import streamlit as st
st.set_page_config(page_title="My App", page_icon="📊", layout="wide")
st.title("Hello, Streamlit!")
st.write("Welcome to your new app.")
EOF
# Run the app
uv run streamlit run app.py
Run Existing Streamlit App
# With uv (preferred)
uv run streamlit run app.py
# With options
uv run streamlit run app.py --server.port 8501 --server.headless true
# With browser disabled (for CI/testing)
uv run streamlit run app.py --server.headless true --browser.serverAddress localhost
Project Structure
Recommended Layout
my-streamlit-app/
├── pyproject.toml # Project config with uv
├── uv.lock # Lock file (commit this)
├── .python-version # Python version pin
├── app.py # Main entry point (or src/app.py)
├── pages/ # Multi-page app pages
│ ├── 1_📊_Dashboard.py
│ ├── 2_📈_Analytics.py
│ └── 3_⚙️_Settings.py
├── components/ # Custom components
│ └── sidebar.py
├── utils/ # Helper functions
│ ├── __init__.py
│ ├── data.py
│ └── charts.py
├── tests/ # Test files
│ ├── conftest.py
│ ├── test_utils.py
│ └── e2e/
│ └── test_app.py
├── .streamlit/ # Streamlit config
│ ├── config.toml
│ └── secrets.toml # (gitignored)
└── data/ # Static data files
pyproject.toml Example
[project]
name = "my-streamlit-app"
version = "0.1.0"
description = "A Streamlit data application"
requires-python = ">=3.10"
dependencies = [
"streamlit>=1.40.0",
"pandas>=2.0.0",
"plotly>=5.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-playwright>=0.5.0",
"ruff>=0.8.0",
]
[tool.uv]
dev-dependencies = [
"pytest>=8.0.0",
"pytest-playwright>=0.5.0",
"ruff>=0.8.0",
]
[tool.ruff]
line-length = 100
target-version = "py310"
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
Testing Strategies
Unit Testing (Logic & Utils)
Test business logic without Streamlit context:
# tests/test_utils.py
import pytest
from utils.data import process_data, validate_input
def test_process_data_with_valid_input():
data = {"value": 10, "multiplier": 2}
result = process_data(data)
assert result == 20
def test_validate_input_rejects_negative():
with pytest.raises(ValueError, match="must be positive"):
validate_input(-5)
Run unit tests:
uv run pytest tests/test_utils.py -v
Testing Streamlit Components with AppTest
Use Streamlit's built-in AppTest for headless testing:
# tests/test_app.py
import pytest
from streamlit.testing.v1 import AppTest
def test_app_loads():
"""Test that the app loads without errors."""
at = AppTest.from_file("app.py")
at.run()
assert not at.exception
def test_sidebar_selection():
"""Test sidebar widget interactions."""
at = AppTest.from_file("app.py")
at.run()
# Interact with selectbox
at.selectbox[0].select("Option B").run()
assert at.session_state["selected"] == "Option B"
def test_button_click_updates_state():
"""Test button click behavior."""
at = AppTest.from_file("app.py")
at.run()
# Click button
at.button[0].click().run()
assert "result" in at.session_state
def test_form_submission():
"""Test form with multiple inputs."""
at = AppTest.from_file("app.py")
at.run()
# Fill form fields
at.text_input[0].input("John Doe").run()
at.number_input[0].set_value(25).run()
at.button("Submit").click().run()
# Check success message appears
assert len(at.success) > 0
E2E Testing with Playwright
For full browser-based testing:
# tests/e2e/test_app.py
import pytest
from playwright.sync_api import Page, expect
import subprocess
import time
@pytest.fixture(scope="module")
def streamlit_app():
"""Start Streamlit app for testing."""
proc = subprocess.Popen(
["uv", "run", "streamlit", "run", "app.py",
"--server.headless", "true", "--server.port", "8599"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
time.sleep(5) # Wait for startup
yield "http://localhost:8599"
proc.terminate()
proc.wait()
def test_homepage_loads(page: Page, streamlit_app: str):
"""Test that homepage renders correctly."""
page.goto(streamlit_app)
expect(page.locator("h1")).to_contain_text("Hello")
def test_sidebar_navigation(page: Page, streamlit_app: str):
"""Test sidebar navigation works."""
page.goto(streamlit_app)
page.click("text=Dashboard")
expect(page).to_have_url(f"{streamlit_app}/Dashboard")
def test_data_input_and_display(page: Page, streamlit_app: str):
"""Test data entry workflow."""
page.goto(streamlit_app)
page.fill("input[aria-label='Enter value']", "42")
page.click("button:has-text('Calculate')")
expect(page.locator(".stSuccess")).to_be_visible()
Run E2E tests:
# Install playwright browsers first
uv run playwright install chromium
# Run E2E tests
uv run pytest tests/e2e/ -v
Test Fixtures for Streamlit
# tests/conftest.py
import pytest
import pandas as pd
from streamlit.testing.v1 import AppTest
@pytest.fixture
def sample_dataframe():
"""Provide sample data for tests."""
return pd.DataFrame({
"name": ["Alice", "Bob", "Charlie"],
"value": [100, 200, 300],
})
@pytest.fixture
def app_test():
"""Provide fresh AppTest instance."""
at = AppTest.from_file("app.py")
at.run()
return at
@pytest.fixture
def app_with_session_state():
"""Provide AppTest with preset session state."""
at = AppTest.from_file("app.py")
at.session_state["user_authenticated"] = True
at.session_state["username"] = "testuser"
at.run()
return at
Development Workflows
Live Development with Hot Reload
# Start with auto-reload (default)
uv run streamlit run app.py
# Disable for production testing
uv run streamlit run app.py --server.runOnSave false
Multi-Page App Development
# pages/1_📊_Dashboard.py
import streamlit as st
st.set_page_config(page_title="Dashboard", page_icon="📊")
st.title("Dashboard")
# Access shared session state
if "data" not in st.session_state:
st.session_state.data = None
# Page-specific content
col1, col2 = st.columns(2)
with col1:
st.metric("Total Users", 1234, delta=56)
with col2:
st.metric("Revenue", "$45,678", delta="12%")
Environment & Secrets Management
# .streamlit/secrets.toml (gitignored)
[database]
host = "localhost"
port = 5432
username = "admin"
password = "secret123"
[api]
key = "sk-..."
# Access in app
import streamlit as st
db_host = st.secrets["database"]["host"]
api_key = st.secrets["api"]["key"]
Docker Deployment
# Dockerfile
FROM python:3.11-slim
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
# Copy dependency files
COPY pyproject.toml uv.lock ./
# Install dependencies
RUN uv sync --frozen --no-dev
# Copy app code
COPY . .
# Expose port
EXPOSE 8501
# Health check
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
# Run app
CMD ["uv", "run", "streamlit", "run", "app.py", \
"--server.port=8501", "--server.address=0.0.0.0", \
"--server.headless=true"]
Build and run:
docker build -t my-streamlit-app .
docker run -p 8501:8501 my-streamlit-app
Common Patterns
Session State Management
import streamlit as st
# Initialize state
if "counter" not in st.session_state:
st.session_state.counter = 0
# Update with callback
def increment():
st.session_state.counter += 1
st.button("Increment", on_click=increment)
st.write(f"Count: {st.session_state.counter}")
Caching Data and Resources
import streamlit as st
import pandas as pd
@st.cache_data(ttl=3600) # Cache for 1 hour
def load_data(url: str) -> pd.DataFrame:
return pd.read_csv(url)
@st.cache_resource # Cache across sessions
def get_database_connection():
return create_connection()
# Use cached data
df = load_data("https://example.com/data.csv")
Error Handling in Apps
import streamlit as st
try:
result = risky_operation()
st.success(f"Operation completed: {result}")
except ValueError as e:
st.error(f"Invalid input: {e}")
except ConnectionError:
st.warning("Connection failed. Please try again.")
if st.button("Retry"):
st.rerun()
except Exception as e:
st.exception(e) # Shows full traceback
Forms for Batch Input
import streamlit as st
with st.form("user_form"):
name = st.text_input("Name")
age = st.number_input("Age", min_value=0, max_value=120)
email = st.text_input("Email")
submitted = st.form_submit_button("Submit")
if submitted:
if not name or not email:
st.error("Name and email are required")
else:
st.success(f"Welcome, {name}!")
# Process form data
Troubleshooting
Common Issues
Port already in use:
# Find and kill process
lsof -i :8501 | grep LISTEN
kill -9 <PID>
# Or use different port
uv run streamlit run app.py --server.port 8502
Import errors with uv:
# Ensure dependencies are synced
uv sync
# Check installed packages
uv pip list
Widget state issues:
# Use key parameter for dynamic widgets
for i, item in enumerate(items):
st.text_input(f"Item {i}", key=f"input_{i}")
Slow app reload:
# Move expensive operations outside main flow
@st.cache_data
def expensive_computation():
# This runs once and is cached
return compute_result()