How To
Feb 28, 2025

How to Validate uv workspace Dependencies

Using uv workspaces makes local development of Python packages much easier, but it requires some care to avoid missing dependencies.

Workspaces let you develop multiple Python packages from a single development environment. This is great for DX, since you can simply run commands on each package using uv run  instead of activating a new virtual environment for each package.

This convenience comes at a cost — missing dependencies can go unnoticed until late in development, or worse, after the packages have already been published.

In Python, this general problem is tricky to solve, as mentioned in the uv documentation:

Python does not provide dependency isolation.

Since Python is interpreted and dynamic, you cannot get a complete ‘build-time’ guarantee about your dependencies. There are a few fundamental challenges for static validation of dependencies:

  1. Transitive dependencies: Package A might depend on B, which depends on C. Package A can import and use C directly without declaring it as a dependency.
  2. Namespace packages: Multiple packages can contribute to the same namespace, making it difficult to determine the true source of an import.
  3. Runtime imports: Python allows importing modules at runtime using importlib or __import__() , which cannot be exhaustively detected in general without running the code.
  4. Shared interpreter state: All packages in a Python environment share the same global interpreter state, so side effects from initializing one package are visible to others.

Even with these barriers, static validation can still help developers catch missing dependencies earlier in the development cycle.

Tach is a tool for Python codebases to manage their 1st-party and 3rd-party dependencies. Using tach check-external or the VSCode extension, undeclared and unused dependencies can be detected immediately. NVIDIA uses Tach for this purpose on their bionemo-framework repo.

Consider this example project:

Example project filetree and pyproject.toml, using uv workspaces with several members.

This project has 4 workspace members in total: workspace-root, coreservice, lib1, and lib2.

Each of these members declares its own dependencies in pyproject.toml, but during development will have access to the full, combined set of dependencies.

However, the intention is that lib packages are standalone libraries which can be published separately, while the coreservice  package depends on these libraries.

pyproject.toml configuration for workspace members

You can validate this relationship is preserved using Tach.

The only configuration necessary to validate 3rd-party dependencies is source_roots, which can use globs to match the standard 'src-layout'. This means adding source_roots = ["**/src"] to your tach.toml config.

Simple tach.toml configuration for src-layout

Then, running tach check-external will automatically detect each project and validate that the declared dependencies match your actual import statements!

If it catches errors, Tach will print out location information and a short description of each issue:

Missing and unused dependencies caught by Tach

In this case, we can see that Tach found undeclared dependencies in lib1 and lib2 . This would have worked fine locally, but caused a runtime error for an end user!

Returning to the barriers mentioned before, Tach makes some assumptions:

  1. Transitive dependencies: Tach requires every import statement to resolve to a declared dependency, and will automatically resolve distribution names to module names. This means that using an undeclared transitive dependency will produce an error. More info here.
  2. Namespace packages: Tach uses the fully normalized import module path to look for a corresponding source file, and it will take the first match across your configured source_roots. This means that import namespace may not behave as expected (if multiple packages belong to the namespace), while import namespace.package will be validated.
  3. Runtime imports: Tach will only look at static import statements, and explicitly does not cover runtime imports.
  4. Shared interpreter state: Tach validates that your source code is written with the expected dependencies, but makes no guarantees about the interpreter state you may expect.

By making these tradeoffs, Tach can run very quickly over large Python codebases, and catch low-hanging dependency issues immediately. It can be run as a pre-commit hook, in CI, and in your editor - there is an official VSCode extension, and a built-in LSP server (tach server) for integration with other editors.

Using workspaces in uv can be a huge gain to developer quality-of-life, but a corresponding investment in code quality is needed. With Tach’s early detection of missing dependencies alongside isolated test suites, you can have confidence that your packages are ready to publish.