Tool uv monorepo
Skill nishide-dev/claude-code-ml-research/skills/tool-uv-monorepo
Comprehensive guide for building Python monorepos with uv workspaces - unified dependency resolution, shared lock files, editable installs, testing strategies, Docker optimization, and CI/CD patterns for managing multiple packages in a single repositoryFrom its SKILL.md
npx -y skills add nishide-dev/claude-code-ml-research --skill tool-uv-monorepoAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
13.1 KB, ~3.1k tokens by cl100k_base, as published. Nobody here has run it
UV Monorepo Development with Workspaces
Complete guide for building and managing Python monorepos using uv's workspace functionality.
Overview
uv's workspace feature enables true monorepo architecture for Python projects, solving the historical challenge of managing multiple packages in a single repository. Inspired by Rust's Cargo, workspaces provide:
- Unified dependency resolution: Single
uv.lockfile for entire repository - Version consistency: Eliminates version drift across packages
- Automatic editable installs: Code changes propagate instantly
- Fast resolution: Rust-powered solver 10-100x faster than pip
- Standard compliance: Built on PEP 621, PEP 735
When to use uv workspaces:
- ✅ Microservices sharing common libraries
- ✅ Multi-package applications (CLI + core + services)
- ✅ Internal library ecosystems
- ✅ Projects requiring strict dependency consistency
Key resources:
- Official docs: https://docs.astral.sh/uv/concepts/projects/workspaces/
- Projects guide: https://docs.astral.sh/uv/
Core Concepts
1. Unified Lock File
The most powerful feature: a single uv.lock at repository root that:
- Resolves all workspace members' dependencies into one conflict-free graph
- Eliminates "version drift" where independent projects use different versions
- Forces all components to use identical versions of shared dependencies (e.g.,
pydantic,fastapi)
When you run uv lock, the system evaluates the entire workspace and creates a mathematically consistent dependency solution.
2. Python Version Constraints
Workspaces enforce a single requires-python for the entire repository, calculated as the intersection of all members' requirements:
| Package | requires-python | Workspace Result |
|---|---|---|
| root | >=3.10 | - |
| service-a | >=3.11 | >=3.11 |
| service-b | >=3.12 | >=3.12 (strictest wins) |
This ensures all members can coexist in the shared virtual environment (.venv).
3. Workspace vs Path Dependencies
Two approaches for managing related packages:
| Feature | Workspace | Path Dependencies |
|---|---|---|
| Lock file | Single uv.lock for all | Separate per project |
| Python version | Unified (intersection) | Independent per project |
| Virtual env | Single shared .venv | Individual .venv per project |
| Consistency | Enforced | Flexible |
| Best for | Tightly coupled services | Highly independent projects |
Directory Structure
Basic Layout
my-monorepo/
├── pyproject.toml # Workspace root config
├── uv.lock # Unified lock file
├── .venv/ # Shared virtual environment
└── packages/
├── core/
│ ├── pyproject.toml
│ └── src/core/
├── api/
│ ├── pyproject.toml
│ └── src/api/
└── cli/
├── pyproject.toml
└── src/cli/
Root Configuration
pyproject.toml (workspace root):
[project]
name = "my-monorepo-workspace" # Must be unique from members!
version = "0.1.0"
requires-python = ">=3.10"
[tool.uv.workspace]
members = ["packages/*"] # Glob patterns
# exclude = ["packages/legacy/*"] # Optional exclusions
[tool.uv]
package = false # Virtual root (not installable)
[dependency-groups]
dev = [
"pytest>=7.4",
"ruff>=0.1",
"mypy>=1.0",
]
Critical: Name Collision
The workspace root name must be unique from all member names. If both root and a member use my-app, uv sync fails with:
Error: Duplicate workspace member: my-app
Use descriptive names: my-app-workspace for root, my-app for actual package.
Package Dependencies
Declaring Internal Dependencies
To make api depend on core, use two-step declaration:
packages/api/pyproject.toml:
[project]
name = "api"
dependencies = [
"core", # Standard PEP 621 declaration
]
[tool.uv.sources]
core = { workspace = true } # uv-specific: resolve from workspace
Why two declarations?
[project.dependencies]: Standard metadata, readable by all tools[tool.uv.sources]: Routing table for uv-specific resolution
If [tool.uv.sources] is missing, uv provides helpful error:
Error: Package 'core' is a workspace member but missing sources entry
Automatic Editable Install
With workspace = true, uv automatically installs members in editable mode. Code changes in core are instantly reflected in api without reinstallation.
Development Dependencies
PEP 735 Dependency Groups
For dev tools (pytest, ruff, mypy), use dependency groups in root:
[dependency-groups]
dev = [
"pytest>=7.4",
"pytest-cov>=4.1",
"ruff>=0.1",
"mypy>=1.0",
]
Benefits:
- Not included in build artifacts (wheels, sdists)
- Shared across all packages:
uv sync --group dev - Prevents version inconsistencies (e.g., different linters per service)
Install:
# Install all dependencies + dev group
uv sync --group dev
# Install only production dependencies
uv sync
Testing with Pytest
Import File Mismatch Problem
In monorepos with multiple tests/ directories, pytest defaults cause errors:
import file mismatch:
imported module 'test_helpers' has this __file__: .../packages/cli/tests/test_helpers.py
which is not the same as: .../packages/core/tests/test_helpers.py
Cause: Pytest's prepend mode inserts test directories into sys.path, causing name collisions.
Solution: Importlib Mode
Root pyproject.toml:
[tool.pytest.ini_options]
addopts = ["--import-mode=importlib"]
This uses Python's importlib to import tests directly without modifying sys.path, eliminating name collisions.
Important: Do NOT add __init__.py to test directories when using importlib mode. This causes silent test skipping (tests are cached under one path but collected from another).
Docker Optimization
Problem: Monolithic Dependencies
A single uv.lock contains dependencies for ALL services. Naive Docker builds include unnecessary dependencies, inflating image size and attack surface.
Solution: Multi-Stage Build with Export
Step 1: Extract Service-Specific Dependencies
FROM ghcr.io/astral-sh/uv:python3.12-alpine AS builder
WORKDIR /app
COPY uv.lock pyproject.toml ./
COPY packages/ ./packages/
# Extract only dependencies for 'api' service
RUN uv export --frozen --directory packages/api -o requirements.txt && \
# Remove workspace member references (e.g., -e ./packages/core)
sed -i '/^-e/d' requirements.txt
Step 2: Install External Dependencies (Cached Layer)
RUN --mount=type=cache,target=/root/.cache/uv \
uv pip sync requirements.txt --no-cache --compile-bytecode
Step 3: Copy Internal Libraries and Application Code
# Copy shared libraries first (changes less frequently)
COPY packages/core/ ./packages/core/
# Copy application code last (changes most frequently)
COPY packages/api/ ./packages/api/
# Install as editable
RUN uv pip install -e ./packages/api/
Benefits:
- External dependencies cached in Docker layer
- Application code changes don't invalidate dependency layer
--compile-bytecodepre-generates.pycfiles for faster startup
Layer Caching Strategy
| Stage | Command | Cache Behavior |
|---|---|---|
| Base | Copy uv binary | Rarely changes |
| Dependencies | uv export + uv pip sync | Only invalidated when uv.lock changes |
| Shared libs | Copy packages/core/ | Invalidated when core changes |
| Application | Copy packages/api/ | Invalidated on every app code change |
CI/CD Best Practices
Global Cache Strategy (GitHub Actions)
Problem: Per-PR caches waste storage and slow down CI.
Solution: Single cache from main branch, read-only for PRs.
Workflow 1: Main Branch (Cache Write)
name: Build Cache
on:
push:
branches: [main]
schedule:
- cron: '0 0 * * 0' # Weekly refresh
jobs:
cache:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
enable-cache: false # Manual cache control
- name: Sync dependencies
run: uv sync --all-groups
- name: Save cache
uses: actions/cache/save@v4
with:
path: |
~/.cache/uv
.venv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
Workflow 2: PR Branches (Cache Read-Only)
name: Test
on: pull_request
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Restore cache
uses: actions/cache/restore@v4
with:
path: |
~/.cache/uv
.venv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-
- uses: astral-sh/setup-uv@v5
- run: uv sync # Uses cache, downloads only diff
- run: uv run pytest
Benefits:
- PRs never pollute cache
- Always clean state from main
- Minimal storage usage
Common Commands
Workspace Management
# Initialize new workspace
uv init --package my-monorepo
cd my-monorepo
# Add workspace member
uv init --lib packages/core
uv init --lib packages/api
# Configure workspace in root pyproject.toml
# Add: [tool.uv.workspace] members = ["packages/*"]
# Sync all dependencies
uv sync
# Sync with dev tools
uv sync --group dev
# Run command in workspace context
uv run python -c "import core; import api"
# Run from specific package directory
cd packages/api
uv run uvicorn main:app
Building & Publishing
# Build specific package
uv build --package api
# Build all packages
uv build --all-packages
# Check version
uv version --package core
# Publish to PyPI
uv publish --package core
Migration from Poly-repo
Pre-Migration: Version Alignment
Critical: Before creating workspace, align all dependency versions across services.
- Update all services to latest compatible versions:
# In each service
uv sync --upgrade
uv run pytest # Verify no breakage
- Resolve version conflicts:
If Service A uses Django 4.2 and Service B uses Django 5.1, choose one version and update all services.
- Create workspace structure:
mkdir -p packages
mv service-a packages/
mv service-b packages/
- Configure root workspace:
Create root pyproject.toml with workspace configuration.
- Convert path dependencies:
Replace relative path dependencies with workspace = true.
Expected outcome: uv lock will surface any hidden version conflicts that were masked by isolated environments.
Troubleshooting
Error: Duplicate workspace member
Cause: Root and member have same name.
Solution: Rename root to project-workspace.
Error: Missing sources entry
Cause: Package in [project.dependencies] but not in [tool.uv.sources].
Solution: Add { workspace = true } to sources.
Pytest import file mismatch
Cause: Default prepend mode + duplicate test file names.
Solution: Add --import-mode=importlib to pytest config.
CUDA/GPU dependencies in monorepo
Solution: Use platform-specific members or path dependencies for GPU-specific code.
Best Practices
- Name virtual roots descriptively:
my-app-workspacenotmy-app - Use dependency groups: Keep dev tools in
[dependency-groups] - Importlib mode always: Set
--import-mode=importlibin pytest config - Docker multi-stage: Use
uv exportto extract service-specific deps - Cache from main: GitHub Actions cache strategy from main branch only
- Align before migration: Update all versions before creating workspace
Summary
uv workspaces provide:
Core Benefits:
- 10-100x faster dependency resolution (Rust-powered)
- Zero version drift through unified lock file
- Instant code propagation with automatic editable installs
- Standard compliance (PEP 621, PEP 735)
- Simplified CI/CD through single environment
Key Features:
- Single
uv.lockfor entire repository - Unified Python version constraint (intersection)
- Shared virtual environment (
.venv) - Automatic workspace member discovery
- Built-in Docker optimization patterns
Resources:
- Official docs: https://docs.astral.sh/uv/concepts/projects/workspaces/
- Practical guide: https://dev.to/aws/3-things-i-wish-i-knew-before-setting-up-a-uv-workspace-30j6
- Advanced CI/CD: https://www.reddit.com/r/Python/comments/1iy4h5k/cracking_the_python_monorepo_build_pipelines_with/
uv workspaces eliminate Python's historical monorepo pain points, bringing Cargo-like simplicity and performance to multi-package projects.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.
Gives 0 of the 12 instructions most architecture codebase skills give in ~3.1k tokens
Counted across 811 of the 1,134 authors here whose files we hold, read 2026-08-07
- Ask the user which candidate to explorein 45 of 811, across 15 files
- Apply the deletion test to suspected shallow modulesin 43 of 811, across 15 files
- Read any relevant architecture decision records firstin 31 of 811, across 8 files
- Use exact glossary terms in every suggestionin 30 of 811, across 10 files
- Accept dependencies instead of creating themin 24 of 811, across 5 files
- Include before and after visualisations for each candidatein 24 of 811, across 5 files
- Read the domain glossary before exploringin 24 of 811, across 6 files
- Return results instead of producing side effectsin 23 of 811, across 4 files
- Explore the codebase for shallow modules and frictionin 23 of 811, across 3 files
- Introduce seams only where things varyin 22 of 811, across 3 files
- Reduce the number of methodsin 21 of 811, across 2 files
- Design deep modules with small interfacesin 21 of 811, across 3 files
Said here and by no other author read
- name virtual workspace roots descriptively
- store development tools in dependency groups
- enable importlib mode for pytest
- export service-specific dependencies for docker builds
- use a main branch cache for CI
- align dependency versions before workspace migration
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.