Use when creating a new Python research project workspace, adding sub-repositories to an existing workspace, or managing cross-repo dependencies with uv workspaces
Manage Python research projects as uv workspaces with git sub-repositories. Each sub-repo is its own git repo and Python package, but all share a single .venv via the workspace. This gives you modularity (reusable code split across repos), reproducibility (locked dependencies), and flexibility (each repo also works standalone).
Research code tends toward either monolithic repos (hard to reuse across projects) or scattered repos (dependency hell, sys.path hacks, duplicated code). The uv workspace pattern solves both:
.venv, one uv sync, all repos see each other@ git+... URLs in dependency lines mean repos work outside the workspace tooThis is the critical technique. When one workspace member depends on another:
# In [project] dependencies — the git URL is used when this repo is standalone
dependencies = [
"my-utils @ git+https://github.com/user/my-utils",
]
# In [tool.uv.sources] — workspace resolution overrides the git URL
[tool.uv.sources]
my-utils = { workspace = true }
How it works:
{ workspace = true } takes priority, resolves to the local checkout[tool.uv.sources] is ignored, the @ git+... URL is usedThis also works for optional dependencies:
[project.optional-dependencies]
extras = [
"OtherRepo @ git+https://github.com/user/OtherRepo",
]
[tool.uv.sources]
OtherRepo = { workspace = true }
mkdir my-project && cd my-project
git init
pyproject.toml[project]
name = "my-project"
version = "0.0.0"
description = "Description of the research project"
requires-python = ">=3.11, <3.13"
[tool.uv.workspace]
members = [
# Add sub-repos here as they are added
]
requires-python should be consistent across all members.Clone existing repos or create new ones (see Operation 2). Add each to the members list.
clone.shA convenience script that clones all member repos:
#!/bin/bash
git clone https://github.com/user/repo1
git clone https://github.com/user/repo2
This is how collaborators (or you on a new machine) reconstruct the workspace.
.gitignore.venv/
__pycache__/
*.egg-info/
CLAUDE.mdCreate a root-level CLAUDE.md that documents the workspace structure, member repos, and conventions. Keep it up to date as the project evolves.
Ask the user if they want a shared IPython kernel for VSCode/Jupyter:
uv run ipython kernel install --user --env VIRTUAL_ENV $(pwd)/.venv --name=my-project
This creates one kernel for the whole workspace. Do not create per-repo kernels.
uv sync --all-packages
git clone https://github.com/user/existing-repo
mkdir new-repo && cd new-repo
git init
Create pyproject.toml:
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "new-repo"
version = "0.0.0"
description = "What this repo does"
requires-python = ">=3.11, <3.13"
dependencies = []
[tool.setuptools.packages.find]
where = ["."]
Add to root pyproject.toml members list:
[tool.uv.workspace]
members = [
"existing-member",
"new-repo",
]
Add to clone.sh
If it depends on other workspace members, use the dual dependency pattern (see below)
If other members depend on it, update their pyproject.toml files too
Verify: uv sync --all-packages
In the dependent repo's pyproject.toml, add both pieces:
[project]
dependencies = [
"other-repo @ git+https://github.com/user/other-repo",
]
[tool.uv.sources]
other-repo = { workspace = true }
[project.optional-dependencies]
extras = [
"OtherRepo @ git+https://github.com/user/OtherRepo",
]
[tool.uv.sources]
OtherRepo = { workspace = true }
A downstream repo can pull in the extras:
dependencies = [
"upstream-repo[extras] @ git+https://github.com/user/upstream-repo",
]
Remove from both dependencies (or optional-dependencies) and [tool.uv.sources].
Always verify:
uv sync --all-packages
If resolution fails, check:
dependencies matches the name in the dependency's pyproject.tomlmembers{ workspace = true } is present in sources for every workspace member that appears in dependencies.venv for the whole workspace. Never create per-repo venvs.uv run — never bare python or pip.requires-python must be consistent across all workspace members.setuptools build backend with [tool.setuptools.packages.find].uv sync --all-packages after any structural change.