Python Packaging: uv CLI Cheatsheet

2026/02/18

BUILDuv🍞

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:

And --frozen means “stop re-solving, I already have the answer.” That’s the one flag worth memorizing.