uv, Lockfiles, and Workspaces

How modern Python projects manage dependencies with uv, and what a workspace is.

uv in one paragraph

uv replaces pip + venv + pip-tools with one fast tool. You declare what you want in pyproject.toml ("nvidia-nat[langchain]>=1.8.0,<1.9"), and uv sync creates .venv/ and installs everything. The C-world analogy: pyproject.toml is the configure.ac — the human-edited statement of requirements — and the environment is the build output. You never edit the environment by hand.

Lockfiles: the exact-versions manifest

uv sync also writes uv.lock: the exact resolved version of every package, direct and transitive, with checksums. The range in pyproject.toml says what’s acceptable; the lockfile says what was actually used.

  • Analogy: pyproject.toml is like linking against libfoo >= 2.x; uv.lock is like shipping the exact .so versions from the box where it worked. Anyone who clones the repo and runs uv sync gets bit-for-bit the same environment.
  • Commit the lockfile. A repo without one can resolve different versions next month and break — “works on my machine” is usually a missing lockfile.
  • Regenerating: uv lock --upgrade (or edit the range and uv sync).

Workspaces: several packages, one environment

A Python project sometimes contains nested packages — e.g. a repo whose tooling scaffolds a subdirectory that is itself an installable package with its own pyproject.toml. A uv workspace ties them together:

# root pyproject.toml
[project]
dependencies = [
    "some_subpackage",          # depend on the nested package by name
]

[tool.uv.sources]
some_subpackage = { workspace = true }   # ...and get it from this repo

[tool.uv.workspace]
members = ["some_subpackage"]            # subdirectories that are packages

One uv sync at the root now installs the framework and the local package, editable, into a single shared .venv/, with one shared uv.lock. Change the subpackage source and the change is live immediately — no reinstall step.

Analogies:

  • Make: a top-level Makefile with SUBDIRS = lib app that builds subprojects into one output tree, instead of cd lib && make install by hand.
  • Git submodules, inverted: a submodule pins someone else’s repo into yours; a workspace member is your own subdirectory promoted to a first-class package. Same repo, one history, one lock.

Without the workspace stanza, the nested package is invisible: uv sync installs only the root project’s dependencies, and imports from the subpackage fail even though the code is sitting right there.

Day-to-day commands

TaskCommand
Create/refresh the environmentuv sync
Run inside the environmentuv run <cmd> (no activate needed)
Add a dependencyuv add <pkg> (edits pyproject.toml + lock)
Upgrade within rangesuv lock --upgrade
Inspect an installed packageuv pip show <pkg>

uv run is the habit worth building: it guarantees the command executes in the project’s environment, like a wrapper script that sets PATH and LD_LIBRARY_PATH before exec’ing the real binary.