Skip to content

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 monorepo

This 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-service

When 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-lib

This 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 lock

The 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 sync

The .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-app needs requests==2.28 and api-service needs requests==2.31, the resolver will fail. In this case, keep the projects in separate repositories or use separate pyproject.toml files outside the workspace.
    • Members need isolated virtual environments. A workspace has one .venv at 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
    Last updated on

    Please submit corrections and feedback...