DEV Community

Cover image for 3 Things I Wish I Knew Before Setting Up a UV Workspace
Dennis Traub for AWS

Posted on

3 Things I Wish I Knew Before Setting Up a UV Workspace

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/
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

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",
]
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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 }`)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode
$ 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%]
Enter fullscreen mode Exit fullscreen mode

Note: Don't add __init__.py to your test directories as a workaround - with importlib mode, 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.


References

Top comments (3)

Collapse
 
soldavidcloud profile image
David Sol

Gracias, muy util

Collapse
 
dennistraub profile image
Dennis Traub AWS

I'm glad I could help!

Collapse
 
klement_gunndu profile image
klement Gunndu

The pytest collision with same-package names across members bit me too β€” spent an embarrassing amount of time before realizing conftest.py was being loaded from the wrong package root.