uv sync. uv add. uv pip install. uv run. uv lock. Which one resolves? Which one downloads? Which one touches the lockfile? Which one touches the venv? And what does --package even scope to?
I keep getting confused. So I sat down and actually mapped out what each command does — what it reads, what it writes, and what it costs you.
Two worlds
First thing to understand: uv has two completely separate interfaces that barely know about each other.
| Interface | Commands | Uses uv.lock? |
Knows about workspaces? |
|---|---|---|---|
| Project | sync, lock, add, remove, run, export |
Yes | Yes |
| pip-compatible | pip install, pip compile, pip sync |
No | No |
| Tool | tool install, tool run / uvx |
No | No |
The project commands are the “new way.” They read pyproject.toml, manage uv.lock, and understand workspaces. The pip commands are the “old way” — fast drop-in replacements for pip/pip-tools that don’t touch the lockfile at all.
Mixing them is where confusion starts. uv pip install requests does NOT update your uv.lock. It just shoves requests into your venv and moves on. Different system entirely.
The operation matrix
Here’s the thing I actually wanted — what does each command read and write?
| Command | Modifies pyproject.toml |
Resolves deps | Modifies uv.lock |
Modifies .venv |
|---|---|---|---|---|
uv lock |
— | yes | yes | — |
uv sync |
— | yes* | yes* | yes |
uv add |
yes | yes | yes | yes |
uv remove |
yes | yes | yes | yes |
uv run |
— | yes* | yes* | yes* |
uv pip install |
— | yes | — | yes |
uv pip compile |
— | yes | — | — |
uv venv |
— | — | — | yes (creates) |
uvx |
— | yes | — | — (ephemeral) |
* Suppressed by --frozen, --locked, or --no-sync.
Notice the pattern. uv lock only resolves. uv sync resolves AND installs. uv add does everything — edits pyproject.toml, re-locks, AND syncs. It’s three operations in a trenchcoat.
What each command actually does
uv lock — resolve, don’t install
Resolves the entire workspace into uv.lock. Downloads package metadata but not the packages themselves. Doesn’t touch your venv.
This is the cheapest “did anything change?” check. And it’s always workspace-wide — you can’t lock just one package.
uv sync — install from lockfile
Re-locks first (unless told not to), then installs into .venv to match the lockfile. Exact sync by default — it removes packages that aren’t in the dependency tree. Surprise.
Creates the venv if it doesn’t exist. This is usually the command you want after cloning a repo.
uv add — the 3-in-1
Adds a dependency to pyproject.toml → re-locks → syncs the venv. Three operations, one command. Use --frozen if you just want the pyproject.toml edit without the resolution and install.
uv run — sync then execute
Ensures the environment is synced, then runs your command. So yes — uv run pytest might re-lock your workspace before running tests. That’s the default. Use --frozen or --no-sync to skip it.
uv pip install — the escape hatch
Installs packages into the active venv. No lockfile. No pyproject.toml. Just raw pip-style installation, but fast. Useful for one-off stuff, but don’t mix it with the project workflow — your venv will drift from your lockfile.
uv venv — create a virtual environment
Creates a .venv directory. That’s it. No resolution, no lockfile, no installing — just an empty virtualenv, fast.
# Default — creates .venv in current directory
uv venv
# Custom path
uv venv my-env
# Specific Python version
uv venv --python 3.12
# Specific Python version + custom path
uv venv --python 3.11 .venv-legacy
You rarely need this with the project workflow — uv sync and uv run create the venv automatically if it’s missing. But it’s useful when you’re doing uv pip install style work and need to set up the venv yourself, or when you want a specific Python version that differs from your project’s requires-python.
One thing that trips people up: uv venv replaces any existing venv at that path without asking. No “are you sure?” prompt. It just nukes and recreates.
uvx / uv tool run — run without installing
Runs a CLI tool in an ephemeral isolated environment. uvx ruff check . doesn’t touch your project at all. Think npx for Python.
The --package flag
This is the monorepo scoping flag. In a workspace with multiple packages, operations default to the root package. --package targets a specific member instead.
uv sync --package my-lib # only install my-lib's deps
uv add requests --package my-lib # add to my-lib's pyproject.toml
uv run --package my-lib pytest # run in my-lib's context
Key subtlety: resolution is always workspace-wide. There’s one uv.lock for the whole workspace. --package only scopes the installation — which subset of the resolved graph goes into your venv.
--frozen vs --locked
These look similar but aren’t.
| Flag | Meaning | Fails when |
|---|---|---|
--frozen |
Use the lockfile as-is. Don’t re-resolve. | Lockfile is missing |
--locked |
Assert the lockfile is up-to-date. Don’t write. | Lockfile is stale |
--frozen is “trust the lockfile, don’t think.” --locked is “verify the lockfile matches pyproject.toml, but don’t update it.”
In CI, you almost always want one of these. Without them, uv sync will happily re-lock and potentially change your lockfile during the build.
# CI: install exactly what's pinned, fail if lockfile missing
uv sync --frozen
# CI: install from lockfile, fail if it's stale
uv sync --locked
The cost of each command
Rough mental model for how expensive each command is:
| Command | Network | Disk I/O | Time |
|---|---|---|---|
uv lock |
Metadata fetches | Write lockfile | Fast (seconds) |
uv sync --frozen |
Download packages | Write to venv | Medium (depends on cache) |
uv sync |
Metadata + packages | Lockfile + venv | Medium-slow |
uv add |
Metadata + packages | pyproject.toml + lockfile + venv |
Medium-slow |
uv run --frozen |
— (if already synced) | — | Instant |
uv run |
Metadata + packages (if stale) | Lockfile + venv | Varies |
uv pip install |
Download packages | Write to venv | Medium |
uv caches aggressively, so repeated operations are fast. The first resolve on a big workspace is the expensive one.
Common patterns
# Day-to-day dev — just run stuff
uv run pytest
uv run python my_script.py
# Add a new dependency
uv add requests
uv add --package my-lib --dev pytest-mock
# CI — reproducible installs
uv sync --frozen
# Docker — layer caching
RUN uv sync --frozen --no-install-project # deps layer (cacheable)
COPY . .
RUN uv sync --frozen # project layer (fast)
# Quick one-off tool
uvx ruff check .
uvx black --check .
# Generate requirements.txt from lockfile
uv export --frozen --format requirements.txt > requirements.txt
The mental model
Here’s how I think about it now:
uv venv= set up an empty roomuv lock= resolve the math problem, write down the answeruv sync= make my venv match the answeruv add= change the problem, re-solve, update my venvuv run= make sure everything’s current, then do the thinguv pip install= forget the lockfile, just put this package in my venvuvx= run this tool, don’t touch my project
And --frozen means “stop re-solving, I already have the answer.” That’s the one flag worth memorizing.