Why use a tool for managing Python versions?
When working across multiple Python projects, you’ll often find that they require different versions of Python. For example, a legacy project may require Python 3.6 while a newer project uses a more recent version like 3.13. You could install these different versions directly to your system by downloading from python.org, but it wouldn’t be long before you’d probably want to reach for a tool to manage Python versions for you.
For years, pyenv has been the Python community’s de facto tool for managing Python installations and was my go-to as well. However, with the rise of uv and its ability to not only manage project dependencies, but now Python itself, I decided to ditch pyenv altogether. These are the steps are took to migrate from pyenv to uv.
Steps
Removing pyenv
The first thing I did was removed all references to pyenv from my machine. I started by removing the ~/.pyenv
directory. This removed all pyenv-managed Python installations.
rm -rf ~/.pyenv
Next, I removed all pyenv initialization scripts that I had configured in my .zshrc
file. Lastly, since I use Homebrew for managing system packages, I used that to remove pyenv completely.
brew uninstall pyenv
Installing uv
Once I confirmed pyenv was uninstalled, it was time to install uv. There are several installation methods available, but as mentioned earlier, I generally reach for Homebrew as often as possible.
brew update && brew install uv
Configuring uv
Once uv was installed, I added some configuration preferences prior to installing Python versions. One of the things I like about pyenv is that it places everything in a single, easily identifiable .pyenv
directory in my HOME
directory. Rather than having binaries and configurations spread across my system, I know that all things pyenv-related can be found in that directory. I wanted to replicate that behavior with uv as much as possible by creating a single ~/.uv
directory. To that aim, I set the following ENV vars in my .zshrc
.
UV_DIR="${HOME}/.uv"
export UV_CACHE_DIR="${UV_DIR}/cache"
export UV_CONFIG_FILE="${UV_DIR}/uv.toml"
export UV_PYTHON_INSTALL_DIR="${UV_DIR}/python/versions"
export UV_PYTHON_BIN_DIR="${UV_DIR}/python/bin"
export PATH="${UV_PYTHON_BIN_DIR}:${PATH}"
UV_CACHE_DIR
: Store cache under$HOME/.uv/cache
.UV_CONFIG_FILE
: Use$HOME/.uv/uv.toml
as a configuration file for uv.UV_PYTHON_INSTALL_DIR
: Install all Python versions in$HOME/.uv/python/versions
.UV_PYTHON_BIN_DIR
: Use$HOME/.uv/python/bin
as the directory for symlinks to Python executables.PATH
: PrependUV_PYTHON_BIN_DIR
to myPATH
to enablepython
andpython3
executables.
After configuring the .uv
directory, I added the following preferences as ENV vars.
export UV_PYTHON_DOWNLOADS=manual
export UV_PYTHON_PREFERENCE=only-managed
UV_PYTHON_DOWNLOADS
: By default, uv will automatically download Python versions when needed. I prefer to be in complete control of when Python is installed on my system, so setting this tomanual
allows Python to be installed only duringuv python install
.UV_PYTHON_PREFERENCE
: Configure uv to only use uv-managed Python installations, not system Python installations.
Once I had all of the configuration I wanted set as ENV vars, I sourced my .zshrc
file. Alternatively, you could just restart your terminal to have the same effect.
source ~/.zshrc
Installing Python
Prior to installing a Python version, I created the ~/.uv/python
directory to store everything uv python
-related, and a uv.toml
file for configuring uv. Currently, this file is empty but will be used in the future for configuring uv tools and projects.
mkdir -p ~/.uv/python && touch ~/.uv/uv.toml
Finally, I installed Python version 3.12
. As of the time of this writing, installing Python executables is in preview mode, so the --preview
option is required (or the UV_PREVIEW
ENV var) for enabling that feature. I also like to have a python
executable in addition to something like python3
or python3.12
, so I added the --default
option in order to create those symlinks in the ~/.uv/python/bin
directory as well.
uv python install 3.12 --preview --default
Now, running uv python list
shows the version I installed and which python
confirms my PATH
now includes the uv-managed Python executable. Now I can run python
anywhere in my shell and be sure that I’m executing only uv-managed Python installations.
Summary
Overall, the switch from pyenv to uv was painless and, even though uv still has yet to release its first major release, the configuration it allows is pretty impressive. I plan on using uv for project dependencies and management as well, so aligning around a single tool for Python projects and installations is a huge gain in developer happiness, at least for me.
There are some caveats to using uv to be aware of, however. For example, it uses pre-built Python distributables instead of building Python from source, like pyenv. I actually prefer that because it means I don’t need to worry about pre-installing or managing system dependencies specific to building CPython from source. The performance gain, for me, is a worthy trade-off for reproducibility in builds but might not work for all teams or projects.