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 (
uvis 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/projectto chdir to a project root directoryuv venvto create a new venv here, that will be inpath/to/project/.venv/echo ". .venv/bin/activate" >.envrcto instructdirenvto activate the venv when entering the projectdirenv allowto telldirenvthat the newly created.envrcshould 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).