Project Setup

uv for Python CLI Dependency Management

Use uv to manage Python CLI dependencies with fast lockfile resolution, virtual env creation, and PEP 621-compliant pyproject.toml workflows.

Updated

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 mycli scaffolds a PEP 621 pyproject.toml with a [project.scripts] entry point.
  • uv add typer adds a dependency and updates uv.lock in one step; uv sync materializes the environment.
  • uv run mycli executes 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.

The uv workflow: uv init, uv add, uv lock, uv sync, uv run — plus uv tool install for end users.

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 lock re-resolves the full graph and writes uv.lock without touching the environment. Run it after editing pyproject.toml by hand.
  • uv sync makes the installed environment match uv.lock exactly — 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: