You can install and manage multiple Python versions on macOS using Homebrew and pyenv, which is a godsend when you’re juggling projects that require different Python environments.

Let’s see this in action. Imagine you need Python 3.9 for an older project and the latest Python 3.12 for a new one.

First, make sure Homebrew is up-to-date. Open your terminal and run:

brew update

Now, install pyenv itself using Homebrew:

brew install pyenv

This command fetches the latest pyenv package and installs it. pyenv works by shimming Python commands, intercepting calls to python, pip, etc., and routing them to the correct version you’ve selected.

Next, you need to configure your shell to use pyenv. Add these lines to your shell’s configuration file (e.g., ~/.zshrc for Zsh, ~/.bash_profile for Bash):

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)"
eval "$(pyenv init -)"

After adding these lines, either restart your terminal or run source ~/.zshrc (or source ~/.bash_profile) to apply the changes.

Now, you can list available Python versions that pyenv can install:

pyenv install --list

This will show a long list. Let’s say you want to install Python 3.9.18 and Python 3.12.3. Run:

pyenv install 3.9.18
pyenv install 3.12.3

These commands download and compile the specified Python versions. This might take a few minutes as it builds Python from source.

Once installed, you can see your local pyenv versions:

pyenv versions

You’ll see something like:

  system
  3.9.18
* 3.12.3 (set by /Users/yourusername/.pyenv/version)

The asterisk * indicates the currently active Python version.

To set a global default Python version (used when no project-specific version is set):

pyenv global 3.12.3

And to set a version for a specific project, navigate to your project’s directory and run:

cd /path/to/your/project
pyenv local 3.9.18

This creates a .python-version file in your project directory containing 3.9.18. pyenv automatically detects this file and switches to that Python version when you’re inside this directory.

Now, if you check your Python version:

python --version

It will output Python 3.9.18 if you’re in the project directory, or Python 3.12.3 if you’re outside it and have set the global default.

The magic of pyenv lies in its shims. When you run python, pyenv intercepts it via a shim executable located in $PYENV_ROOT/shims/python. This shim checks your PYENV_VERSION environment variable, the .python-version file in the current directory or its parents, and finally your global pyenv setting to determine which Python executable to run. This allows for seamless switching without modifying your system’s PATH directly for each Python version.

The most surprising thing about pyenv is how it manages virtual environments. While pyenv-virtualenv is a common plugin, pyenv itself doesn’t require separate virtual environments for each Python version to function. It manages the Python interpreters directly. However, for package management within a specific Python version, using virtual environments is still best practice. You can create one for your project using the active pyenv version:

python -m venv .venv
source .venv/bin/activate

This creates a .venv directory with isolated packages for your current Python version.

If you ever encounter an issue where pyenv commands aren’t recognized after installation, double-check that the eval "$(pyenv init -)" line is correctly placed in your shell’s startup file and that you’ve sourced it.

The next challenge you’ll likely face is managing package dependencies across these different Python versions, which is where tools like Poetry or Pipenv become invaluable.

Want structured learning?

Take the full Homebrew course →