Python Monorepo with uv Workspaces
Modern Python monorepo architecture using uv for workspace management and mise for Python version and task orchestration.
Core Concepts
Monorepo: Single repository containing multiple related packages and applications
uv workspace: Python's answer to npm/pnpm workspaces
- Single lock file for entire repo
- Shared virtual environment
- Cross-package dependency resolution
Directory Structure
my-monorepo/
├── .mise.toml # Python version + task runner
├── pyproject.toml # Root workspace config
├── uv.lock # Unified lock file
├── apps/ # Deployable applications
│ ├── api/
│ └── worker/
└── packages/ # Shared libraries
├── core/
└── utils/
Workspace Configuration
Root pyproject.toml:
[project]
name = "my-monorepo"
version = "0.1.0"
requires-python = ">=3.12"
[tool.uv.workspace]
members = ["apps/*", "packages/*"]
[dependency-groups]
dev = [
"pytest",
"ruff",
"basedpyright",
]
See references/workspace-config.md for detailed configurations.
Package Linking
Workspace packages reference each other by distribution name:
packages/utils/pyproject.toml:
[project]
name = "my-utils"
dependencies = ["my-core"]
[tool.uv.sources]
my-core = { workspace = true }
apps/api/pyproject.toml:
[project]
name = "my-api"
dependencies = ["my-core", "my-utils", "fastapi>=0.100.0"]
[tool.uv.sources]
my-core = { workspace = true }
my-utils = { workspace = true }
Cross-Package Import
packages/core/src/my_core/entities.py:
class User:
def __init__(self, email: str):
self.email = email
apps/api/src/my_api/main.py:
from my_core.entities import User # Import from workspace package
def run() -> None:
# App code...
Dependency Direction (Critical)
apps/ → packages/ (Apps depend on packages)
packages/ ⇏ apps/ (Never the reverse)
Rules:
- Apps can depend on packages
- Packages can depend on other packages
- Packages should not normally depend on deployable apps; allow an exception only when the architecture explicitly requires it.
- Avoid circular dependencies
mise Task Runner
.mise.toml:
[tools]
python = "3.14"
[tasks.check]
depends = ["lint", "typecheck", "test"]
[tasks.lint]
run = "uv run ruff check ."
Usage: mise run check
Core uv Commands
uv sync # Root project and its dependency closure
uv sync --all-packages # Every workspace member
uv add fastapi --package my-api # Add to specific package
uv add my-core --package my-api # Add workspace package
uv run pytest # Run tests
uv lock --upgrade # Update dependencies
Best Practices
- Single lock file at root
- Shared dev tools in root dependency groups
- Pin Python version with mise
- Apps depend on packages only
- Use namespace packages for logical grouping (optional)
References
For detailed patterns:
- Workspace configuration - sources, commands, and version policy
- Docker - reproducible, non-root workspace images
- Namespace packages - PEP 420 package layouts