| name | python-dependency-management |
| description | How to manage Python dependencies with UV โ inline script metadata (PEP 723), project mode, tool running, and when to use each approach. Always use UV; never pip directly. |
| paths | **/*.py,**/pyproject.toml,**/*.toml,**/*.sh |
Python Dependency Management with UV
Rule: Always use uv. Never call pip directly. Never create virtualenvs manually.
Mode 1 โ Inline script metadata (PEP 723)
For standalone scripts that don't belong to a project. UV reads the # /// script block and creates a temporary isolated environment automatically.
import requests
from rich import print
Run:
uv run script.py
./script.py
uv run --python 3.12 script.py
When to use: One-off scripts, exploratory scripts, scripts shipped as single files, CI helper scripts. No pyproject.toml needed.
With extra deps at run time (without editing the file):
uv run --with rich --with requests script.py
With a specific package version pinned temporarily:
uv run --with "cadquery==2.4.0" script.py
Mode 2 โ Project mode (pyproject.toml)
For packages, services, or anything with multiple files and a test suite.
uv init my-project
cd my-project
uv add requests rich
uv add --dev pytest ruff
uv sync
uv run pytest
uv run my-project
Lock file is uv.lock โ commit it. Reproducible installs on all machines.
uv sync --frozen
uv lock --upgrade-package requests
Lockfile hygiene โ pin the index in the project, not the machine
uv.lock records the index URL each package came from. So a machine
configured against a private or corporate mirror rewrites every URL in the lock
on any resolve โ and uv run resolves by default:
uv run pytest
uv run --frozen pytest
That produces a diff of thousands of near-identical lines that nobody reads, and
git add -A sweeps it straight in. On a public repo the consequences are worse
than noise: it publishes an internal hostname, and it breaks CI, which cannot
reach the mirror.
Diagnose before blaming the tool. It is almost never a patched or forked uv.
Check, in order:
which -a uv && uv --version
env | grep -i '^UV_\|^PIP_'
cat ~/.config/uv/uv.toml
cat ~/.config/pip/pip.conf
A user-level index-url in ~/.config/uv/uv.toml applies to every project on
the machine, public ones included. That is the bug: private configuration in a
global scope.
Fix it in the project. uv's precedence is CLI > environment > project config >
user config > system config, so the repo can override the machine โ which means
the fix is committed and protects every contributor, every CI run, and every agent
session, instead of depending on someone remembering an env var:
[[tool.uv.index]]
url = "https://pypi.org/simple"
default = true
Verify it rather than assuming: with the user config still pointing at the mirror,
a bare uv run pytest should leave uv.lock untouched.
uv run pytest && git status --short
Prefer this to the alternatives: export UV_DEFAULT_INDEX=โฆ and habitual
uv run --frozen both depend on remembering them every session, and --frozen
additionally hides a genuinely stale lock. .envrc/direnv isn't committed, so it
only protects whoever has direnv set up.
The mirror-image fix is worth doing too: move the private index config out of the
user-level file and into the private repos that need it. Private configuration
belongs where the private code is.
Guard it in CI, since prevention and detection are different jobs:
- run: uv lock --check
- run: |
if grep -n 'registry = "' uv.lock | grep -v 'https://pypi.org/simple'; then
echo "::error file=uv.lock::uv.lock references a non-public index"; exit 1
fi
Note that uv lock --check fails on the mirrored machine and passes in CI,
because CI has no user config. If it fails locally, re-run it with the public
index forced before concluding the lock is genuinely stale:
UV_DEFAULT_INDEX=https://pypi.org/simple uv lock --check
Mode 3 โ Tool running (uvx)
For CLI tools you want to run without polluting the project or global env.
uvx ruff check .
uvx black .
uvx cadquery-server
uvx --from cadquery cq-cli ...
uvx = uv tool run โ installs into an isolated cache, reuses on subsequent calls.
Install a tool globally (available in PATH):
uv tool install ruff
uv tool upgrade ruff
uv tool list
Mode 4 โ pip compatibility shim
When you must interact with a requirements.txt or use pip-style commands (legacy projects, CI scripts):
uv pip install -r requirements.txt
uv pip compile requirements.in -o requirements.txt
uv pip sync requirements.txt
Avoid this mode for new projects โ use Mode 2 instead.
Choosing the right mode
| Situation | Mode |
|---|
| One-off script, no project structure | Mode 1 โ inline # /// script metadata |
| Exploring a library interactively | Mode 1 โ uv run --with lib script.py |
| Package / service / multi-file project | Mode 2 โ pyproject.toml + uv add |
| Running a CLI tool once | Mode 3 โ uvx tool-name |
Integrating with legacy requirements.txt | Mode 4 โ uv pip |
Environment and Python version management
uv python install 3.12
uv python list
uv venv --python 3.12
UV manages its own Python downloads under ~/.local/share/uv/python/.
Common patterns
Script that renders a mesh (inline deps):
import trimesh, matplotlib.pyplot as plt
CI install (reproducible):
uv sync --frozen --no-dev
Upgrade all deps:
uv lock --upgrade
uv sync
Check what's installed:
uv pip list
uv tree