How to set up a Python monorepo with uv workspaces
A uv workspace lets multiple Python packages share a single repository, a single lockfile, and a single virtual environment. This is the standard approach for monorepos where packages depend on each other and need to stay in sync.
Create the workspace root
Initialize a new project to serve as the workspace root:
uv init monorepo
cd monorepoThis creates a pyproject.toml with standard project metadata. The root project acts as the workspace container; it does not need to contain any application code itself.
Add workspace members
Create packages inside a packages/ directory. Use --lib for libraries (packages meant to be imported) and --app for applications (packages meant to be run):
mkdir -p packages
cd packages
uv init --lib shared-lib
uv init --app worker-app
uv init --app api-serviceWhen uv init detects a pyproject.toml in a parent directory, it automatically adds the new package as a workspace member. If no [tool.uv.workspace] table exists yet, uv creates one. After running these commands, the root pyproject.toml will contain:
[tool.uv.workspace]
members = [
"packages/shared-lib",
"packages/worker-app",
"packages/api-service",
]Use glob patterns for members
Listing each member individually becomes tedious as the workspace grows. Replace the explicit list with a glob:
[tool.uv.workspace]
members = [
"packages/*",
]Any directory under packages/ that contains a pyproject.toml will be treated as a workspace member. New packages added later are picked up automatically.
Wire up cross-package dependencies
To make worker-app depend on shared-lib, use uv add with the --package flag:
uv add --package worker-app shared-libThis adds two entries to packages/worker-app/pyproject.toml:
[project]
dependencies = [
"shared-lib",
]
[tool.uv.sources]
shared-lib = { workspace = true }The workspace = true source directive tells uv to resolve shared-lib from the local workspace instead of PyPI. The library is installed in editable mode, so changes to shared-lib are immediately visible to worker-app without reinstalling.
Lock and sync the workspace
A workspace uses a single uv.lock at the root. Running uv lock resolves dependencies for all members together:
uv lockThe lockfile records every member and its resolved dependencies. This guarantees that all packages in the workspace use the same version of every shared dependency.
To install everything into the workspace’s shared virtual environment:
uv syncThe .venv directory lives at the workspace root, not inside individual members.
Run commands in specific packages
Use --package to target a specific member:
uv run --package worker-app python -c "from shared_lib import hello; print(hello())"This ensures the command runs with worker-app and its dependencies available. The same flag works with other uv commands like uv add and uv remove.
Realistic example
Consider a monorepo with a shared library used by two applications:
- pyproject.toml
- uv.lock
- pyproject.toml
- init.py
- pyproject.toml
- main.py
- pyproject.toml
- main.py
The root pyproject.toml defines the workspace:
[project]
name = "monorepo"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[tool.uv.workspace]
members = [
"packages/*",
]Both worker-app and api-service depend on shared-lib:
# packages/worker-app/pyproject.toml
[project]
name = "worker-app"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"shared-lib",
]
[tool.uv.sources]
shared-lib = { workspace = true }Running uv lock produces a single lockfile that resolves shared-lib along with any external dependencies from both applications.
Know when not to use workspaces
Workspaces work well when all members can share a single virtual environment and a single lockfile. They are the wrong tool when:
- Members need conflicting dependency versions. All workspace members resolve together. If
worker-appneedsrequests==2.28andapi-serviceneedsrequests==2.31, the resolver will fail. In this case, keep the projects in separate repositories or use separatepyproject.tomlfiles outside the workspace. - Members need isolated virtual environments. A workspace has one
.venvat the root. If packages must run in separate environments (for example, because they target different Python versions), workspaces cannot provide that isolation.
Tip
To exclude a directory from an otherwise broad glob, use the exclude key:
[tool.uv.workspace]
members = ["packages/*"]
exclude = ["packages/legacy-app"]Get Python tooling updates
Subscribe to the newsletter