I love uv, it's so much better than pip, but I'm still learning the ins and outs. Today I was setting up a Python monorepo with uv workspaces and ran into a few issues, the fixes of which were trivial once I knew about them.
1. Give the Root a Distinct Name
First, a virtual root (package = false) still needs a [project] name - and it can't match any member package.
I had both the root and my core package using the same name, e.g. my-app:
my-app/ # workspace root
pyproject.toml # name = "my-app" <- problem!
packages/
core/
pyproject.toml # name = "my-app"
src/core/
cli/
pyproject.toml # name = "my-app-cli"
src/cli/
When I ran uv sync, it refused outright:
$ uv sync
error: Two workspace members are both named `my-app`:
`/path/to/my-app` and `/path/to/my-app/packages/core`
Even though the root has package = false, uv still registers its name as a workspace member identity. Same name, two members, no way to disambiguate.
The fix - give the root a workspace-specific name:
# Root pyproject.toml
[project]
name = "my-app-workspace" # NOT "my-app"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[tool.uv]
package = false
[tool.uv.workspace]
members = ["packages/*"]
[dependency-groups]
dev = [
"pytest",
"ruff",
]
Two things to note: package = false means "don't install me", not "don't need a name". And dev dependencies go in [dependency-groups] (PEP 735), not [project.dependencies] - the root is virtual, so project dependencies are just metadata.
2. Use workspace = true for Inter-Package Deps
When one workspace package depends on another, you need two things: a normal dependency declaration and a [tool.uv.sources] entry telling uv to resolve it locally.
# packages/cli/pyproject.toml
[project]
name = "my-app-cli"
dependencies = [
"my-app",
]
[tool.uv.sources]
my-app = { workspace = true }
Without the [tool.uv.sources] entry, uv sync fails with a helpful but initially confusing error:
$ uv sync
x Failed to build `my-app-cli @ file:///path/to/packages/cli`
|-- Failed to parse entry: `my-app`
\-- `my-app` is included as a workspace member, but is missing
an entry in `tool.uv.sources`
(e.g., `my-app = { workspace = true }`)
At least uv tells you exactly what to add.
The [project.dependencies] list stays PEP 621 compliant, so any standard Python tool can read it. The [tool.uv.sources] table is uv-specific and only affects resolution. And uv sync installs the local package as editable automatically - changes are immediately visible without reinstalling.
3. Use importlib Mode for pytest
When running pytest across a workspace where multiple packages have tests/ directories with same-named test files (e.g. both have test_helpers.py), pytest's default import mode breaks:
$ uv run pytest packages/ -v
collected 1 item / 1 error
ERROR collecting packages/core/tests/test_helpers.py
import file mismatch:
imported module 'test_helpers' has this __file__ attribute:
/path/to/packages/cli/tests/test_helpers.py
which is not the same as the test file we want to collect:
/path/to/packages/core/tests/test_helpers.py
HINT: remove __pycache__ / .pyc files and/or use a unique basename
Pytest's default prepend import mode treats both test_helpers.py as the same module. It imports the first one, caches it, then errors when the second file doesn't match.
The fix - add importlib mode to your root pyproject.toml:
# Root pyproject.toml
[tool.pytest.ini_options]
addopts = "--import-mode=importlib"
$ uv run pytest packages/ -v
packages/cli/tests/test_helpers.py::test_cli_helper PASSED [ 50%]
packages/core/tests/test_helpers.py::test_core_helper PASSED [100%]
Note: Don't add
__init__.pyto your test directories as a workaround - withimportlibmode, that can actually cause a silent bug where pytest resolves both files to the same cached module and runs the wrong tests without any error.
This isn't uv-specific - it's a Python monorepo thing. But uv workspaces make monorepos easy to set up, so you're likely to hit it early.
';" />
';" />
';" />