5.4 KiB
5.4 KiB
Use uv for Python Development, pip for Docker Builds
- Status: accepted
- Date: 2026-02-02
- Deciders: Billy Davies
- Technical Story: Standardizing Python package management across development and production
Context and Problem Statement
Our Python projects use a mix of requirements.txt and pyproject.toml for dependency management. Local development with pip is slow, and we need a consistent approach across all repositories while maintaining reproducible Docker builds.
Decision Drivers
- Fast local development iteration
- Reproducible production builds
- Modern Python packaging standards (PEP 517/518/621)
- Lock file support for deterministic installs
- Compatibility with existing CI/CD pipelines
Considered Options
- pip only (traditional)
- Poetry
- PDM
- uv (by Astral)
- uv for development, pip for Docker
Decision Outcome
Chosen option: "uv for development, pip for Docker", because uv provides extremely fast package resolution and installation for local development (10-100x faster than pip), while pip in Docker ensures maximum compatibility and reproducibility without requiring uv to be installed in production images.
Positive Consequences
- 10-100x faster package installs during development
uv.lockprovides deterministic dependency resolutionpyproject.tomlis the modern Python standard (PEP 621)- Docker builds remain simple with standard pip
uv pip compilecan generaterequirements.txtfrompyproject.toml- No uv runtime dependency in production containers
Negative Consequences
- Two tools to maintain (uv locally, pip in Docker)
- Team must install uv for local development
- Lock file must be kept in sync with pyproject.toml
Pros and Cons of the Options
pip only (traditional)
- Good, because universal compatibility
- Good, because no additional tools
- Bad, because slow resolution and installation
- Bad, because no built-in lock file
- Bad, because
requirements.txtlacks metadata
Poetry
- Good, because mature ecosystem
- Good, because lock file support
- Good, because virtual environment management
- Bad, because slower than uv
- Bad, because non-standard
pyproject.tomlsections - Bad, because complex dependency resolver
PDM
- Good, because PEP 621 compliant
- Good, because lock file support
- Good, because fast resolver
- Bad, because less adoption than Poetry
- Bad, because still slower than uv
uv (by Astral)
- Good, because 10-100x faster than pip
- Good, because drop-in pip replacement
- Good, because supports PEP 621 pyproject.toml
- Good, because uv.lock for deterministic builds
- Good, because from the creators of Ruff
- Bad, because newer tool (less mature)
- Bad, because requires installation
uv for development, pip for Docker (Chosen)
- Good, because fast local development
- Good, because simple Docker builds
- Good, because no uv in production images
- Good, because pip compatibility maintained
- Bad, because two tools in workflow
- Bad, because must sync lock file
Implementation
Local Development Setup
# Install uv (one-time)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Create virtual environment and install dependencies
uv venv
source .venv/bin/activate
uv pip install -e ".[dev]"
# Or use uv sync with lock file
uv sync
Project Structure
my-handler/
├── pyproject.toml # PEP 621 project metadata and dependencies
├── uv.lock # Deterministic lock file (committed)
├── requirements.txt # Generated from uv.lock for Docker (optional)
├── src/
│ └── my_handler/
└── tests/
pyproject.toml Example
[project]
name = "my-handler"
version = "1.0.0"
requires-python = ">=3.11"
dependencies = [
"handler-base @ git+https://git.daviestechlabs.io/daviestechlabs/handler-base.git",
"httpx>=0.27.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.0",
"ruff>=0.1.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Dockerfile Pattern
The Dockerfile uses uv for speed but installs via pip-compatible interface:
FROM python:3.13-slim
# Copy uv for fast installs (optional - can use pip directly)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
# Install from pyproject.toml
COPY pyproject.toml ./
RUN uv pip install --system --no-cache .
# OR for maximum reproducibility, use requirements.txt
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
Generating requirements.txt from uv.lock
# Generate pinned requirements from lock file
uv pip compile pyproject.toml -o requirements.txt
# Or export from lock
uv export --format requirements-txt > requirements.txt
Workflow
- Add dependency: Edit
pyproject.toml - Update lock: Run
uv lock - Install locally: Run
uv sync - For Docker: Optionally generate
requirements.txtor useuv pip installin Dockerfile - Commit: Both
pyproject.tomlanduv.lock
Migration Path
- Create
pyproject.tomlfrom existingrequirements.txt - Run
uv lockto generateuv.lock - Update Dockerfile to use pyproject.toml
- Delete
requirements.txt(or keep as generated artifact)
Links
- uv Documentation
- PEP 621 - Project Metadata
- Astral (uv creators)
- Related: handler-base already uses uv in Dockerfile