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.tomlis like linking againstlibfoo >= 2.x;uv.lockis like shipping the exact.soversions from the box where it worked. Anyone who clones the repo and runsuv syncgets 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 anduv 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
MakefilewithSUBDIRS = lib appthat builds subprojects into one output tree, instead ofcd lib && make installby 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
| Task | Command |
|---|---|
| Create/refresh the environment | uv sync |
| Run inside the environment | uv run <cmd> (no activate needed) |
| Add a dependency | uv add <pkg> (edits pyproject.toml + lock) |
| Upgrade within ranges | uv lock --upgrade |
| Inspect an installed package | uv 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.