uv has recently attracted many interest, in particular because it replaces in one tool many existing tools while being much faster. On the other hand, I’ve used pyenv for years but it was sometimes a bit slower than I’d like. In particular it has a visible overhead at every shell prompt. So I decided to see if uv could replace pyenv. (Spoiler: yes.)

Pros:

Cons:

What I replace

I have many projects, each one with it’s own virtual env managed with pyenv. Using pyenv, all these venvs reside in ~/.pyenv/, and at each project root directory, I have a file .python-version that makes pyenv load the venv whenever I cd into the project.

With uv each venv will be directly at the project root, which I feel tidier. Then, we will use direnv to automatically load the venv when entering the project.

Per-project venvs

Creating and setting up a new venv is the matter of four shell commands:

This is equivalent to running pyenv virtualenv myenv && pyenv local myenv except that we don’t have to care about fetching a new venv name for each project.

To remove a venv, just rm -rf .venv .envrc and it’s gone.

It is also possible to add stuff to .envrc to customize further the venv. In particular, I often add export CC=gcc CXX=g++ because uv venvs seem to use clang by default which fails to compile several modules I’m using.

Global venvs

I’ve also setup a few venvs in ~/.local/venv/, which I can activate using a .envrc with . ~/.local/venv/xxx/bin/activate on a per-directory fashion as before. I also use a script venv to run a command after activating a venv:

#!/bin/sh
set -e
VENV="$1"
shift
if [ -d "$VENV" ]
then
  . "$VENV/activate"
elif [ -d "$HOME/.local/venv/$VENV" ]
then
  . "$HOME/.local/venv/$VENV/bin/activate"
else
  echo "venv not found: $VENV"
  exit 1
fi
exec "$@"

For instance, running venv xxx python starts the Python interpreter from venv xxx.

The venvs we don’t need anymore

I had many venvs just to run one tool, for instance, yt-dlp was installed in a pyenv-managed venv, and I had a script to activate the venv and run yt-dlp. This can be replaced with uv run --with yt-dlp yt-dlp, and an alias will make it even easier to use. (And in this case, we always run the latest version which is interesting.)

The venvs we cannot replace

I have a few venvs with Python 2.7 to run legacy tools that was never ported to newer Python versions. These ones could not be created by uv because it looks unable to install unsupported Python versions, which can be understood but is a pity in my case. So for them, I use .envrc to start pyenv and activate my old venv.

Bonus

After this, I’ve installed starship prompt to have a prompt with the activated venv nicely displayed. Starship is really nice, has a very low latency, and is easy to customize. The venv name that is displayed can be customized by editing the prompt entry in .venv/pyvenv.cfg for each venv (by default it will be the name of the directory in which the venv has been created).