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:
- faster (
uv
is indeed very fast)- cleaner and lighter (just regular virtual envs)
- many venvs are not needed anymore (using
uv run --with ...
)Cons:
- cannot have venvs with unsupported Python versions (for legacy tools)
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:
cd path/to/project
to chdir to a project root directoryuv venv
to create a new venv here, that will be inpath/to/project/.venv/
echo ". .venv/bin/activate" >.envrc
to instructdirenv
to activate the venv when entering the projectdirenv allow
to telldirenv
that the newly created.envrc
should be trusted and used
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).