Python Packaging #4 uv: The Standard Workflow of 2026
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:
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# or
brew install uvA new project starts with init:
uv init invoice-tool
cd invoice-toolWhat 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:
uv add django
uv add --dev pytest ruffWhat 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:
git clone .../invoice-tool && cd invoice-tool
uv syncThe .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:
uv run python main.py
uv run pytestuv 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.
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 versionsWith 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”:
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:
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.txtIf 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 removesynchronize declaration (pyproject.toml), lock (uv.lock), and environment (.venv) in one step - After cloning,
uv syncalone reproduces the environment, anduv runexecutes commands in the project environment without activate uv python install/pinputs 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) anduv 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).