Python Packaging #4 uv: The Standard Workflow of 2026

5 min read

List everything we’ve done by hand so far: create a venv, activate, pip install, record versions, manage the Python version separately. This post is about what happens when one tool takes over all of it. As of 2026, uv is the de facto standard workflow for managing Python projects. Built in Rust by Astral, the makers of ruff, it passed its 1.0 release in 2025 and overtook the incumbent tools in monthly downloads. It’s also why this blog’s testing and automation series and Modern Python Basics #1 assumed uv from the start. Its speed is famous (several to tens of times faster than older tools for lock-file installs), but the practical value is less the speed than the workflow folding into one.

Installation and the first project: uv init #

uv itself is a single executable with no Python dependency, so installation is simple:

Install
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# or
brew install uv

A new project starts with init:

Create project
uv init invoice-tool
cd invoice-tool

What gets created is modest: the pyproject.toml from #3, a .python-version pinning the Python version, plus main.py and a README. No .venv yet — uv creates it the moment it’s needed.

add and sync: declaration and environment, always in step #

Adding dependencies is add:

Add dependencies
uv add django
uv add --dev pytest ruff

What happens behind that one line is a summary of the earlier posts: the declaration is recorded in pyproject.toml’s dependencies (or the dev dependency-group), the full dependency set is resolved and locked into uv.lock, and if .venv doesn’t exist it gets created and installation completes. Declaration (#3), lock (#5), and environment (#1) synchronized in one command. Removal is symmetric: uv remove django.

When you clone a colleague’s repository, it’s one command:

Reproduce environment
git clone .../invoice-tool && cd invoice-tool
uv sync

The .venv is reproduced exactly as the lock file says. The README no longer needs a procedure list like “install Python X.Y, create a venv, activate, pip install -r…”.

run: execution without activate #

In #1 we saw that activate is nothing but PATH manipulation. uv lets you skip the step entirely:

Run
uv run python main.py
uv run pytest

uv run finds the project’s .venv (creating it if missing, reconciling it if it drifted from the lock) and executes the command in that environment. The entire incident class of “forgot to activate and installed into the wrong environment” disappears structurally. This is why the testing series used uv run pytest as the default invocation.

uv manages Python versions too #

uv’s scope doesn’t end at packages: it downloads and manages Python interpreters themselves.

Manage Python versions
uv python install 3.13     # install an interpreter
uv python pin 3.13         # pin for the project (writes .python-version)
uv python list             # installed and available versions

With a .python-version present, every uv command operates on that version, and a missing version is fetched automatically. This absorbs the role pyenv used to play — the three-tool set of “Python version manager + virtual environment tool + package manager” collapses into one. The #1 principle of never touching the system Python holds naturally.

uvx and uv tool: the right home for global tools #

Here is the answer #1 deferred with “never install into the system Python; global tools have their own way”:

Run and install global tools
uvx ruff check .            # one-off run, no install
uv tool install ruff        # install as a global tool (isolated environment)

uvx runs a tool immediately in a temporary isolated environment; uv tool install installs frequently used tools into per-tool isolated environments exposed on PATH. Either way, neither your project environment nor the system Python gets polluted. Tools of your own, like the CLI built in Automation #7, install the same way.

Migrating an existing project #

Moving a requirements.txt project over is closer to absorption:

Migration
uv init --bare              # add just a pyproject.toml to existing code
uv add -r requirements.txt  # absorb the list into dependencies
uv add --dev -r requirements-dev.txt

If it’s a freeze-style list from #2, this is the moment to prune it down to direct dependencies. The lock manages transitive dependencies, so there’s no reason to keep them in the declaration. The reverse direction works too, for environments where uv can’t be adopted (uv export --format requirements.txt). For hands used to pip’s command set there’s also a low-level interface (uv pip install), but for project management, add/sync is the proper path — it keeps declaration, lock, and environment aligned together.

Summary #

What we covered in this post:

  • uv bundles virtual environments, package installation, locking, and Python version management into one tool — the de facto standard for new projects in 2026
  • Start with uv init; uv add/uv remove synchronize declaration (pyproject.toml), lock (uv.lock), and environment (.venv) in one step
  • After cloning, uv sync alone reproduces the environment, and uv run executes commands in the project environment without activate
  • uv python install/pin puts interpreters under uv’s management. The team’s Python version is pinned via .python-version, no pyenv needed
  • Global tools install isolated via uvx (one-off) and uv tool install (permanent). The system Python stays untouched to the end
  • Existing projects are absorbed with uv add -r requirements.txt; prune freeze-style lists down to direct dependencies

In the next post (#5 dependency locking) we read the contents of uv.lock — the file quietly built behind every add. How locking guarantees reproducibility, upgrade strategy, and the standard that connects tools: pylock.toml (PEP 751).

X