Workspace Support in uv provides Cargo-style monorepo functionality, allowing multiple Python projects to coexist in a single repository with shared dependencies and unified dependency resolution. This page documents the workspace discovery mechanism, configuration, member management, and workspace-aware operations.
For information about lockfile format and generation, see Lockfile Management. For dependency operations within workspaces, see Dependency Operations. For project-level commands, see Project Commands.
Workspace Discovery and Structure: The workspace system begins with root discovery, searches for workspace configuration, discovers members via glob patterns, validates member pyproject.toml files, and caches the results for reuse.
Sources: crates/uv-workspace/src/workspace.rs1-300
The workspace root is identified by searching upward from the current directory until finding a pyproject.toml with a [tool.uv.workspace] table, or an implicit single-project workspace.
Workspace Root Discovery Algorithm: Starting from the current directory, search upward for a pyproject.toml. If it has [tool.uv.workspace], it's an explicit workspace root. If it only has [project], it's an implicit single-project workspace. Continue upward unless a stop condition is met.
Sources: crates/uv-workspace/src/workspace.rs175-245
The DiscoveryOptions struct controls workspace discovery behavior:
| Option | Type | Description |
|---|---|---|
stop_discovery_at | Option<PathBuf> | Path to stop upward search |
members | MemberDiscovery | Strategy for discovering members (All, None, or Ignore specific paths) |
project | ProjectDiscovery | Whether [project] table is required (Optional, Legacy, or Required) |
Sources: crates/uv-workspace/src/workspace.rs86-142
Member Discovery Process: Workspace members are discovered by expanding glob patterns from tool.uv.workspace.members, filtering excluded paths, validating each member has a valid pyproject.toml, checking for nested workspaces, and ensuring no duplicate package names.
Sources: crates/uv-workspace/src/workspace.rs246-450
The Workspace struct represents a complete workspace:
Sources: crates/uv-workspace/src/workspace.rs148-172
Each member in the workspace is represented by a WorkspaceMember:
Members can be:
[project] table and produce installable packages[project] tableSources: crates/uv-workspace/src/workspace.rs980-1050
To avoid re-reading pyproject.toml files, workspace discovery results are cached:
Sources: crates/uv-workspace/src/workspace.rs29-43
The ToolUvWorkspace struct parses this configuration:
Sources: crates/uv-workspace/src/pyproject.rs670-695
The Sources enum specifies where dependencies come from:
| Source Type | Format | Description |
|---|---|---|
Workspace | { workspace = true } | Another workspace member |
Git | { git = "url", ... } | Git repository |
Path | { path = "..." } | Local file path |
Url | { url = "..." } | Direct URL |
Registry | { registry = "..." } | Package index |
CatchAll | { index = "..." } | Catch-all for other formats |
Sources: crates/uv-workspace/src/pyproject.rs741-845
Workspace Discovery Sequence: The workspace discovery checks the cache first. On cache miss, it finds the root, expands globs to find members, reads their pyproject.toml files, validates the structure, caches the result, and returns the workspace.
Sources: crates/uv-workspace/src/workspace.rs175-300
Workspace Dependency Resolution: Dependencies from all members are combined. Sources and indexes are merged with member-specific configurations overriding workspace defaults. A single universal resolution is performed, resulting in one lockfile for the entire workspace.
Sources: crates/uv-workspace/src/workspace.rs500-700 crates/uv-resolver/src/lock/mod.rs232-375
The uv sync command can sync specific members or the entire workspace:
Sync Process:
pyproject.toml and uv.lock--package flags or default to all.venv to match lockfileSources: crates/uv/tests/it/sync.rs204-378
Adding Dependencies: When adding a dependency in a workspace, uv detects the workspace, determines the target member (current directory), updates the member's pyproject.toml, updates sources if needed, re-locks the entire workspace, and syncs the environment.
Sources: crates/uv/tests/it/edit.rs28-143 crates/uv-workspace/src/pyproject_mut.rs1-250
Workspace members can depend on each other. The required_members field tracks inter-member dependencies:
Where Editability is:
Some(true): Member must be installed as editableSome(false): Member must be installed as non-editableNone: No constraint on editabilityConflict Detection: If a member is requested as both editable and non-editable, an error is raised:
Sources: crates/uv-workspace/src/workspace.rs148-172 crates/uv-workspace/src/workspace.rs46-84
When one member depends on another, it uses the workspace = true source:
This is parsed into:
Sources: crates/uv-workspace/src/pyproject.rs741-800
Workspace Lockfile Generation: All member dependencies are collected, the union of requires-python constraints is computed, sources are merged (member-specific overriding workspace-level), and a single universal resolution is performed. The resulting lockfile contains virtual packages for workspace members and regular packages for external dependencies.
Sources: crates/uv-resolver/src/lock/mod.rs232-375
Workspace members appear in the lockfile as virtual packages:
The virtual source indicates a workspace member, with the path relative to the workspace root.
Sources: crates/uv/tests/it/lock.rs18-147
| Error | Condition | Resolution |
|---|---|---|
MissingPyprojectToml | No pyproject.toml found in hierarchy | Create a pyproject.toml with [project] or [tool.uv.workspace] |
MissingPyprojectTomlMember | Member matched by glob lacks pyproject.toml | Add pyproject.toml to member or adjust glob pattern |
MissingProject | pyproject.toml exists but no [project] table | Add [project] table or use [tool.uv.workspace] for virtual workspace |
NestedWorkspace | Member has [tool.uv.workspace] table | Remove workspace definition from member |
DuplicatePackage | Multiple members have same project.name | Rename one of the conflicting packages |
EditableConflict | Member requested as both editable and non-editable | Resolve conflicting editable specifications |
Sources: crates/uv-workspace/src/workspace.rs46-84
The find_workspace function implements the core discovery logic:
Steps:
PyProjectToml[tool.uv.workspace] or [project]stop_discovery_at or filesystem rootWorkspaceCache for reuseSources: crates/uv-workspace/src/workspace.rs175-245
The check_nested_workspaces function ensures no nested workspace roots:
For each member, it checks if the member's pyproject.toml has a [tool.uv.workspace] table. If found, returns WorkspaceError::NestedWorkspace.
Sources: crates/uv-workspace/src/workspace.rs900-950
The check_package_duplicates function ensures unique package names:
It verifies that each project.name appears only once across all members. If duplicates are found, returns WorkspaceError::DuplicatePackage with both paths.
Sources: crates/uv-workspace/src/workspace.rs950-1000
The Workspace type provides methods to convert to resolver inputs:
These are consumed by the resolver to perform workspace-wide dependency resolution.
Sources: crates/uv-workspace/src/workspace.rs500-700
The lockfile generation consumes workspace information:
The root parameter is the workspace root, and the resolution includes all workspace member dependencies. The resulting lock contains both virtual packages (workspace members) and external packages.
Sources: crates/uv-resolver/src/lock/mod.rs232-375
The sync command uses workspace information to determine what to install:
When package is specified, only those members and their dependencies are installed. Otherwise, all workspace members are synced.
Sources: crates/uv/tests/it/sync.rs204-378
Refresh this wiki