bossjones avatar

streamlit-uv-development

Use this skill when developing Streamlit applications with uv package manager. Covers project setup,

作者 bossjones|オープンソース

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()

Resources