Updating Python

MP 147: How do you clean up when you have too many Pythons lying around?

About a year and a half ago, I wrote a post about how to update the version of Python you're using locally. I was hoping that would be a guide I could refer back to, as I built a better habit of staying up to date. But shortly after that post came out, I started using uv to manage my local Python versions. I've spent the last year and a half using uv to build virtual environments, but now I have a mix of uv- and pyenv-based Python versions lying around. I want to simplify things, and just use uv from this point forward.

I imagine I'm not the only one with a number of outdated Python versions lying around, so I'm going to write up my experience cleaning up all the Pythons on my system. If you want to clean up your system as well you probably won't follow these exact steps, but your overall process will probably be somewhat similar to this.

What do I want?

Before removing or installing anything, I want to re-evaluate my needs for local versions of Python:

  • I write a bunch of small scripts that don't use any third-party libraries, and don't need a dedicated virtual environment. I want to have a python command on my system that always points to the latest point release of the most recent stable version of Python. At the time of this writing, that's Python 3.13.5.
  • I want to easily be able to create virtual environments based on any version of Python I might need, from older versions like Python 3.8 to release candidates like Python 3.14.0rc1.
  • I may want to have commands like python3.12, python3.11, and maybe a few other versions available on my system. I test programs on these older versions regularly enough, that it might be nice not to have to create virtual environments every time I want to run one of these versions.

If you're cleaning up your system, take a moment to figure out what you actually need in your local Python versions these days. My needs have definitely changed over the years, in part due to the kinds of work I'm involved in, and in part due to the changes in tooling. My work has become more varied and complex, but the tools for managing different Python versions have gotten much better. So, I should end up with a simpler setup even though my needs have gotten more complicated.

It's also a good to check out Python's status page, and see what the latest point releases are for each major release:

bar chart showing release date and end of support date for each major Python release
Python's status page is fantastic for knowing which versions are still being supported, and when the next version will be released. As of this writing, Python 3.9 is just about to reach end of life, and Python 3.14 will be released this fall.

At this point, Python 3.9 is just about to reach end of life, and Python 3.14 is almost ready for its initial release.

What do I have?

Before making any changes, I want to take a quick inventory of what I have on my system. I'm pretty sure I've only used pyenv or uv to install Python on this system, so I should be able to use those tools to see what I have.

First, let's see what my current python command points to:

$ python -V
Python 3.12.8
$ which python
/Users/eric/.pyenv/shims/python

The -V flag shows the current version, and the which command shows the path to the actual python executable. Here I can see my main python command is pointing to 3.12.8, and it's managed by pyenv. I'm going to get rid of this, because I don't want anything related to pyenv on my system at this point.

Note: To be clear, I haven't had any problems with pyenv. I have just really enjoyed using uv, and it does everything I was using pyenv for, in a simpler workflow.

Now let's see what else I used pyenv to install:

$ pyenv versions
  system
  3.11.8
* 3.12.8 (set by /Users/eric/.pyenv/version)
  3.13.1

I'm going to make sure all these, except system, are removed.

I think I might have used homebrew to install Python at some point. Let's check that out:

$ brew list | grep python
python-packaging
python@3.11
python@3.13

I seem to have used brew to install Python 3.11 and 3.13 at some point. I'll make sure to remove those as well.

Let's make sure I know where the system Python is. I really don't want to remove that, so verifying its current location is probably a good idea.

$ /usr/bin/python3 -V
Python 3.9.6

This is the usual path to the system Python on modern versions of macOS, and it's there on my system. That's good.

Cleaning up

I might have some more Python installations lying around, but I'm going to start cleaning up my system before looking for anything else. I know I don't want pyenv anymore, so I'm going to get rid of that first.

Removing pyenv

I think you can just uninstall pyenv and that should get rid of the versions it installed, but I'm going to do a little more work and uninstall those versions myself first:

$ pyenv versions
  system
  3.11.8
* 3.12.8 (set by /Users/eric/.pyenv/version)
  3.13.1
$ pyenv uninstall 3.11.8 3.12.8 3.13.1
pyenv: remove /Users/eric/.pyenv/versions/3.11.8? (y/N) y
pyenv: 3.11.8 uninstalled
pyenv: remove /Users/eric/.pyenv/versions/3.12.8? (y/N) y
pyenv: 3.12.8 uninstalled
pyenv: remove /Users/eric/.pyenv/versions/3.13.1? (y/N) y
pyenv: 3.13.1 uninstalled

With pyenv, you can uninstall multiple versions of Python in one command. pyenv will confirm the removal of each version.

Now I shouldn't see any pyenv versions recognized on my system:

$ pyenv versions
  system

Okay! That looks good.

Now I'm going to follow the official directions for uninstalling pyenv`. For me, that meant removing some pyenv-specific lines from my ~/.zshrc file. I also needed to remove the root pyenv directory:

~$ ls -alh | grep pyenv
drwxr-xr-x     6 eric  staff   192B Nov 14  2024 .pyenv
~$ ls -alh .pyenv 
total 24
drwxr-xr-x    2 eric  staff    64B Jul 31 21:32 shims
-rw-r--r--    1 eric  staff     7B Jan  2  2025 version
drwxr-xr-x    3 eric  staff    96B Jul 31 21:32 versions

The only pyenv-related directory in my home folder is .pyenv, and it has three subdirectories. pyenv uses shims to manage which python commands point to which Python executables. Removing the .pyenv directory should remove all remaining pyenv-specific files from my system.

~$ rm -rf .pyenv
~$

And now I'll use homebrew to uninstall pyenv:

$ brew uninstall pyenv
Uninstalling /opt/homebrew/Cellar/pyenv/2.6.3... (1,333 files, 4.2MB)

Now let's try my python command again:

$ python -V
zsh: command not found: python

Okay, that's good, because python was pointing to an interpreter installed by pyenv. But it's a little scary to think I might not have python pointing to anything. There are some system utilities on macOS that depend on having a system Python installed. I think this is just an alias issue, so let's make sure the system Python is still there:

$ /usr/bin/python3 -V
Python 3.9.6

Okay, it's still there. I think the system utilities all use the full path to the interpreter, so I don't think I'm going to see any issues from not having the python command point to anything at the moment.

Removing homebrew Pythons

Now let's remove those homebrew Python versions:

$ brew list | grep python  
python-packaging
python@3.11
python@3.13
~$ brew uninstall python@3.11
Uninstalling /opt/homebrew/Cellar/python@3.11/3.11.13... (3,306 files, 62.1MB)
~$ brew uninstall python@3.13
Error: Refusing to uninstall /opt/homebrew/Cellar/python@3.13/3.13.5
because it is required by lilypond and pipx, which are currently installed.
You can override this and force removal with:
  brew uninstall --ignore-dependencies python@3.13

That's interesting. I was able to remove 3.11 without any issues. But I have two packages installed by homebrew that seem to depend on a homebrew-supplied version of Python 3.13. I might be able to point those packages to a uv-supplied Python, but I don't want to get into that now. I'm okay with having one up-to-date Python interpreter on my system provided by homebrew.

Installing Python with uv

Now I want to install an up to date version of Python with uv, and use that as my default interpreter for general Python work.

First, let's update uv:

$ brew upgrade uv
==> Upgrading uv
  0.7.12 -> 0.8.4

Now I can install the latest version of Python:

$ uv python install
Installed Python 3.13.5 in 930ms
 + cpython-3.13.5-macos-aarch64-none (python3.13)

If you don't specify a version, uv will install the latest stable release. (Note that it's uv python install, not uv install python!)

But, my python commands are still blank, or pointing to brew-installed interpreters:

$ which python
python not found
$ which python3
/opt/homebrew/bin/python3
$ which python3.13
/opt/homebrew/bin/python3.13

You can find the location of your uv-managed Python interpreters with the dir command:

$ uv python dir
/Users/eric/.local/share/uv/python

I want to point my python command to the interpreter that uv just installed. That should be a one-line alias in my .zshrc file:

alias python="$HOME/.local/share/uv/python/cpython-3.13.5-macos-aarch64-none/bin/python3.13"

Now my python command is working again, and it points to the uv-installed Python:

$ python -V
Python 3.13.5
~$ which python
python: aliased to /.../uv/.../cpython-3.13.5-.../bin/python3.13

I ended up removing this alias a short time later, after finding that it conflicted with the python command in active virtual environments. We'll get to that in a moment.

More uv pythons!

Now I can give myself a bunch of interpreters:

$ uv python install 3.12 3.11 3.10 3.9
Installed 4 versions in 1.64s
 + cpython-3.9.23-macos-aarch64-none (python3.9)
 + cpython-3.10.18-macos-aarch64-none (python3.10)
 + cpython-3.11.13-macos-aarch64-none (python3.11)
 + cpython-3.12.11-macos-aarch64-none (python3.12)

I don't need to make aliases for these, because uv is on my path and it doesn't conflict with any brew-installed Python versions. So now I can call any interpreter I want:

$ python3.13 -V
Python 3.13.5
$ python3.12 -V
Python 3.12.11
$ python3.11 -V
Python 3.11.13
$ python3.10 -V
Python 3.10.18
$ python3.9 -V
Python 3.9.23

And with those installed, why not go ahead and install the pre-release version of 3.14 as well:

$ uv python install 3.14
Installed Python 3.14.0rc1 in 923ms
 + cpython-3.14.0rc1-macos-aarch64-none (python3.14)
$ python3.14 -V
Python 3.14.0rc1

This is great! I can run any Python script outside a virtual environment with any version of Python I want, and I can start a terminal session using any of these interpreters as well. I just spent a couple days doing a close reading of all the Python release notes for versions 3.11 through 3.14. Being able to jump into any of these interpreters is a great way to explore the differences between recent versions of Python.

Virtual environments are easy with uv

Just to be clear, it should be easy to create a virtual environment with uv, using any version of Python you want. For example, let's say I wanted to see a bug that a user reported using Python 3.12.8.

Here's how to make a virtual environment with any point release of Python, even if you haven't already installed that version:

$ uv venv .venv --python=3.12.8
Using CPython 3.12.8
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate
$ source .venv/bin/activate
(.venv)$ python -V
Python 3.13.5

The --python flag lets you specify any Python you want when creating a new virtual environment. But this isn't quite working here. I can see that 3.12.8 is being used to create the virtual environment. But when I activate the environment and use the python command, I'm getting 3.13.5. I'm pretty sure that's happening because of the python alias I created earlier.

Let's check:

(.venv)$ which python
python: aliased to /.../uv/python/cpython-3.13.5-.../bin/python3.13

Yes, that's the issue. I could do something to make sure that alias only takes effect outside virtual environments. But I probably don't need a general python command on my system anymore. With all those easily-accessible python3.x commands, I should just name the specific version I want to use any time I run a script outside a virtual environment.

This also has the advantage that I'll always know which version of Python I'm using. I should be able to completely avoid those annoying situations where you're accidentally using a different version than what you thought you were. From now on, python should only ever point to an interpreter in an active virtual environment. If I ever use python outside a virtual environment, I'll get an error and either use a more specific python3.x command, or activate the environment I meant to use.

After removing the python alias from ~/.zshrc, I can make a 3.12.8 environment that works as it should:

$ rm -rf .venv 
$ uv venv .venv --python=3.12.8
Using CPython 3.12.8
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate
$ source .venv/bin/activate
(.venv)$ python -V
Python 3.12.8

If I need to use python in a terminal, ie for teaching purposes, I can always use a temporary alias:

$ alias python=python3.13
$ python -V
Python 3.13.5

This alias will only last as long as that terminal window is open, and I can unset it or point it to a different interpreter any time I need.

More consistent updates

One of my long-term goals has been to more consistently update my Python versions as new point releases come out. uv makes that much easier; take a look at their documentation about upgrading installed versions.

Since I'm using uv to manage all my interpreters, I can now upgrade every installed version with one command:

$ uv python upgrade
All versions already on latest supported patch release

This is incredibly helpful, and it's part of why people are so enthusiastic about uv, and the work that Astral is doing in general. We want everyone to be on secure, well-supported versions of Python, and tools like this make it much easier for everyone to do so.

That said, I will review uv's upgrading documentation again before deciding how to upgrade. If you read that documentation carefully, there are a couple ways you can handle how the older point releases are treated during upgrades. As of this writing, the default behavior is to keep the old versions installed, so that virtual environments pointing to those interpreters are still functional. I don't want a bunch of old interpreters piling up, and I don't mind rebuilding virtual environments. So I'll probably delete all existing Python interpreters and reinstall them, or use a command that replaces the old point releases instead of leaving them in place.

All these approaches are much simpler than they used to be, so I'm actually starting to look forward to the next upgrades! At any point, you can see all your installed versions with the list command:

$ uv python list --only-installed

This actually showed me a few more older point releases I had laying around. I cleaned those up, so I'm moving forward with only one interpreter per Python version.

For the record, I love grep; here's a quick command that shows only the uv-installed interpreters on my system, without any extra lines showing redundant links to this core set of interpreters:

$ uv python list --only-installed | grep uv/python | grep -v local/bin
/.../uv/python/cpython-3.14.0rc1-macos-aarch64-none/bin/python3.14
/.../uv/python/cpython-3.13.5-macos-aarch64-none/bin/python3.13
/.../uv/python/cpython-3.12.11-macos-aarch64-none/bin/python3.12
/.../uv/python/cpython-3.11.13-macos-aarch64-none/bin/python3.11
/.../uv/python/cpython-3.10.18-macos-aarch64-none/bin/python3.10
/.../uv/python/cpython-3.9.23-macos-aarch64-none/bin/python3.9

I can scan this output, see exactly how many versions I currently have installed, and see which point release each version is at as well.

I won't be surprised to find other cruft from various ways of installing Python on my system over the years. I'll remove the unused cruft as I find it, but I'm also not too worried about those remnants interfering with my current setup. The system Python is pretty well isolated, and the ones I want to use can be updated or removed at any point, using fairly straightforward uv commands. Also, this post is a great reference when I need to clean up my system again. Most Python writers I know are writing partly for an external audience, but also to have a clear record of their own work.

Conclusions

At this point, I have a simpler but more flexible Python environment on my system. Every interpreter is installed by uv. Each interpreter is available either through a version-specific command such as python3.13, or in an active virtual environment through the command python.

If your system has accumulated a bunch of Python cruft, consider whether you can move to a uv-managed approach as well. If you're just getting started with Python and looking for a good way to manage your environment, I highly recommend starting with uv. Even if you're not using uv, it's probably a good idea to reevaluate how you manage your environment from time to time, update your workflows, and clean out the most obvious cruft from your system.