uv is an extremely fast Python package and project manager written in Rust. For CLI development it replaces the whole pip + virtualenv + pip-tools stack — and often Poetry too — with a single binary that resolves dependencies, manages a project virtual environment, writes a cross-platform lockfile, and installs your tool onto the user's PATH. This hub walks the commands you reach for daily when building and shipping a Python command-line tool with uv.
TL;DR
uv init --package mycliscaffolds a PEP 621pyproject.tomlwith a[project.scripts]entry point.uv add typeradds a dependency and updatesuv.lockin one step;uv syncmaterializes the environment.uv run mycliexecutes your console script inside the managed venv without manual activation.uv tool install .installs your CLI globally so end users can run it anywhere.- For a head-to-head with the other popular bootstrapper, see uv init vs poetry init for CLI tools.
Scaffolding a project: uv init
uv init creates the project skeleton. For a CLI you want the --package flag, which lays out an importable src/ package and registers a console-script entry point:
uv init --package mycli
cd mycli
This produces a PEP 621 pyproject.toml — the standardized project metadata table that any modern build backend understands:
[project]
name = "mycli"
version = "0.1.0"
description = "A friendly command-line tool."
requires-python = ">=3.9"
dependencies = []
[project.scripts]
mycli = "mycli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
The [project.scripts] table is the standard way to declare CLI entry points — the same syntax covered in best practices for Python CLI entry points.
Adding dependencies: uv add, uv lock, uv sync
uv add is the command you use most. It records the dependency in pyproject.toml, re-resolves the dependency graph, and updates uv.lock atomically:
uv add "typer>=0.12"
uv add --dev pytest ruff
Development-only tools go behind --dev, landing in a [dependency-groups] table rather than your runtime dependencies. Two lower-level commands sit underneath uv add:
uv lockre-resolves the full graph and writesuv.lockwithout touching the environment. Run it after editingpyproject.tomlby hand.uv syncmakes the installed environment matchuv.lockexactly — installing what's missing and removing what shouldn't be there.
The lockfile: uv.lock
uv.lock is a universal, cross-platform lockfile. Unlike a flat requirements.txt, it captures resolutions for every platform and Python version your project supports, so a single committed lockfile reproduces identically on Linux, macOS, and Windows. Commit it to version control. Because the lock is the source of truth, CI can install with a single reproducible command:
uv sync --frozen
--frozen errors out if uv.lock is stale relative to pyproject.toml, which is exactly what you want in CI — it guarantees nobody forgot to re-lock.
Managing the virtual environment
uv creates and manages a project venv at .venv/ automatically — you rarely create one by hand. The first uv run or uv sync provisions it, and uv will even download a managed CPython build if your requires-python isn't satisfied locally. If you do want an explicit environment, uv venv creates one, but for project work you can skip it entirely and let the commands below handle activation transparently.
Running code: uv run
uv run executes a command inside the project environment, syncing it first if needed — no source .venv/bin/activate required:
uv run mycli --help
uv run pytest
uv run python -c "import mycli; print(mycli.__file__)"
This is the single most useful day-to-day command. Because it auto-syncs, uv run mycli always reflects the current pyproject.toml and uv.lock, which makes it ideal for both local iteration and CI scripts.
Distributing the CLI: uv tool install
Once your tool is ready to use as a global command, uv tool install installs it into an isolated environment and puts its entry points on your PATH — the modern equivalent of pipx install:
uv tool install my-cli-tool --from .
mycli --version
Each installed tool gets its own venv, so global CLIs never clash over conflicting dependencies. For quick one-off invocations of a published tool without installing it permanently, uvx mycli (an alias for uv tool run) fetches and runs it in a throwaway environment.
Where to go next
uv and Poetry both bootstrap projects, declare entry points, and lock dependencies, but they make different trade-offs around speed and lockfile format. For a detailed, side-by-side decision guide with concrete pyproject.toml snippets, read:
- uv init vs poetry init for CLI tools — bootstrapping speed, lockfile strategy, and entry-point syntax compared.
Related
- Project Setup & Dependency Management — the parent pillar covering scaffolding, versioning, and packaging.
- Poetry workflows for CLI development — the same lifecycle managed with Poetry instead of uv.
- uv init vs poetry init for CLI tools — choose between the two bootstrappers.