Skip to content

Virtual Environments with virtualenv and pyenv

DEPRECATED - replaced with:

Virtual Environments with uv

Using Python with Virtual Environments

Original Content

Creating a virtual environment

python3 -m venv .venv

Activate Virtual Environment

Linux and macOS - Activate

source .venv/bin/activate

Windows - Activate

Using PowerShell:

.venv\Scripts\activate.ps1

Deactivate Virtual Environment

Linux and macOS - Deactivate

deactivate

Windows - Deactivate

deactivate

Using uv or pyenv to Manage Different Versions of Python

Overview

If you find yourself needing to manage 4 or 5 different versions for different projects, you might eventually outgrow aliases. Most developers eventually move to a tool called pyenv.

It handles the switching automatically based on which folder you are standing in. If you enter Project_A, it flips to 3.11; if you move to Project_B, it flips to 3.12.

Since you have a mix of system, local, and multiple Homebrew versions, using pyenv is definitely the smartest move. It acts as a "traffic controller" that intercepts the python3 command and routes it to the specific version you want, without you ever having to touch your $PATH again.

Here is how to set it up to manage those specific versions.

  1. Install pyenv via Brew

Even though you already have the Python versions installed, pyenv works best when it manages the installations itself. However, it can also "see" your existing Homebrew versions.

First, install the tool:

brew install pyenv
  1. The "Magic" Configuration

To make pyenv take control, you must add these lines to your ~/.zshrc (or ~/.bash_profile). This creates "shims"—tiny interceptor scripts that decide which Python to run.

# Add to ~/.zshrc
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

Save and restart your terminal or run source ~/.zshrc.

  1. Let pyenv Manage Your Versions

While you have versions in /opt/homebrew/bin, pyenv prefers to install them into its own directory (~/.pyenv/versions) to ensure they don't break when Homebrew updates.

To install your specific versions via pyenv:

pyenv install 3.12.0
pyenv install 3.13.0
pyenv install 3.14.0

To determine which versions of Python are available:

pyenv install --list | grep "3.12"
3.12.0
3.12-dev
3.12.1
3.12.2
3.12.3
3.12.4
3.12.5
3.12.6
3.12.7
3.12.8
3.12.9
3.12.10
3.12.11
3.12.12
3.12.13
3.13.12
3.13.12t
  1. How to Switch (The Best Part)

Now that pyenv is in charge, you have three ways to "force" a version:

Global (The "New Default"): Changes the version for your entire computer (unless overridden).

pyenv global 3.13.0

Local (Per-Project): If you are inside a specific project folder, run this once. Every time you enter that folder in the future, it automatically switches to that version.

bash pyenv local 3.12.0

Shell (Temporary): Changes the version only for your current terminal window.

```bash
pyenv shell 3.14.0
```

Comparison of your Python paths

Scenario Path returned by "which python3" Actual Version Running
With pyenv ~/.pyenv/shims/python3 Managed by pyenv
System Default /usr/bin/python3 macOS Internal (Old)
Manual brew /opt/homebrew/bin/python3.12 Static/hardcoded

Troubleshooting the "Ghost" Python

Because you have a version in /usr/local/bin/python3 (likely from a manual installer from Python.org), that file often tries to "outrank" others. By putting the pyenv init lines in your .zshrc, you are telling the computer: "I don't care what is in /usr/local/bin; check the shims first."

Manage Python Upgrades

With pyenv, the best practice is to treat each version—even minor "patch" updates like moving from 3.12.0 to 3.12.9—as a distinct, separate installation.

You should not try to install "over the top" in the traditional sense, and you don't actually need to uninstall the old one first unless you are tight on disk space.


  1. How pyenv handles "Versions"

pyenv keeps every version in its own unique folder inside ~/.env/versions/. Because of this:

3.12.0 lives in ~/.pyenv/versions/3.12.0
3.12.9 lives in ~/.pyenv/versions/3.12.9

They do not overwrite each other. This is a safety feature: if your code breaks on the newer version, you can instantly switch back to the old one.

  1. The Recommended Workflow

Instead of overwriting, simply install the latest patch and update your "Global" pointer.

  • Check for the latest available 3.12 patch:
pyenv install --list | grep " 3.12"
  • Install the newest one (e.g., 3.12.9):
pyenv install 3.12.9
  • Update your Global version:
pyenv global 3.12.9
  • Optional: Remove the old one
pyenv uninstall 3.12.0

The "Gotcha": Packages don't migrate automatically

This is the most important part: Your installed libraries (like pandas or requests) stay with the specific version they were installed for.

If you move from 3.12.0 to 3.12.9, your new 3.12.9 environment will be blank. You have two choices:

  • The Manual Way: Re-install your requirements in the new version.
  • The Pro Way: Use the pyenv-migrate plugin or simply export your current list:
# While on 3.12.0
pip freeze > requirements.txt

# Switch to 3.12.9
pyenv global 3.12.9
pip install -r requirements.txt

Why this is better than "Overwriting"

If you were using a standard installer that overwrote files, a failed installation could leave you with a broken Python environment and no way to run your code. With pyenv, the old version remains perfectly functional until the very moment you decide to delete it.

Python Package Management - Post-Upgrade

Managing Python versions can feel like a chore, but you can automate the migration of your packages so that upgrading from 3.12.0 to 3.12.9 is a one-command affair.

The most reliable way to do this is by using a combination of pip exports and a small shell function.

  1. The "One-Liner" Migration

If you just installed a new version (e.g., 3.12.9) and want to pull everything from your old version (3.12.0), run this command:

pyenv shell 3.12.0 && pip freeze > requirements.txt && pyenv shell 3.12.9 && pip install -r requirements.txt && rm requirements.txt
  1. Automate it with a Shell Function

Instead of typing that long string every time, you can add a "helper" to your ~/.zshrc file. This is much cleaner and acts like a custom command.

Open your config: nano ~/.zshrc

Paste this function at the bottom:

```bash # Custom function to migrate packages between pyenv versions

py-migrate() { local old_ver=\(1 local new_ver=\)2

echo "Changing to $old_ver..." pyenv shell $old_ver pip freeze > /tmp/migrate_reqs.txt

echo "Switching to $new_ver and installing packages..." pyenv shell $new_ver pip install --upgrade pip pip install -r /tmp/migrate_reqs.txt

rm /tmp/migrate_reqs.txt echo "Migration complete!" } ```

Save and Reload: source ~/.zshrc

  1. Using the pyenv-pip-migrate Plugin

If you want a "pro" tool that lives inside pyenv itself, there is a community plugin specifically for this.

Installation:

git clone https://github.com/pyenv/pyenv-pip-migrate.git $(pyenv root)/plugins/pyenv-pip-migrate

Usage:

To migrate from 3.12.0 to 3.12.9, you simply run:

pyenv pip-migrate 3.12.0 3.12.9
  1. A Note on "Global" Tools

For tools that aren't specific to a project (like flake8, black, or yt-dlp), many developers use a tool called pipx.

pipx installs these tools in their own isolated pockets that persist regardless of which pyenv version you are currently using. It prevents you from having to re-install your favorite utilities every time you jump from Python 3.12 to 3.13.

Python Virtual Environments & pyenv

Using pyenv and virtual environments are not mutually exclusive; in fact, they are the "dynamic duo" of Python development.

Think of it this way: pyenv manages the Engine (the Python version), while Virtual Environments manage the Cargo (your project's specific libraries).

  1. How they interact

When you use pyenv, it changes which python executable is active in your terminal. When you create a virtual environment after setting a pyenv version, that environment is "born" from that specific version.

The Workflow:

  • Set the version: pyenv local 3.12.9
  • Create the env: python -m venv .venv
  • Activate: source .venv/bin/activate

Now, even if you change your pyenv global to 3.14.0 later, that specific .venv folder will stay locked to 3.12.9. It carries a copy (or symlink) of the Python binary from the moment it was created.

  1. The pyenv-virtualenv Plugin

While the standard venv module works perfectly with pyenv, there is a popular plugin called pyenv-virtualenv that integrates them even more tightly.

Why people use it:

  • Centralized Storage: Instead of having .venv folders scattered inside every project, all your environments live inside ~/.pyenv/versions/.
  • Auto-Activation: You can tell pyenv to automatically activate an environment the moment you cd into a project folder.

Example Setup:

# Create an environment named 'my-project-env' based on 3.12.9
pyenv virtualenv 3.12.9 my-project-env

# Set it to auto-activate in the current folder
pyenv local my-project-env
  1. Comparison: Standard venv vs. pyenv-virtualenv
Feature Standard python -m venv pyenv-virtualenv Plugin
Location Inside your project folder Inside ~/.pyenv/versions/
Activation Manual (source .venv/bin/activate) Automatic via .python-version file
Visible to pyenv No Yes (shows up in pyenv versions)
Best For Standardized, portable projects Heavy multitaskers / Power users

The "Gold Standard" Setup

Most modern developers use pyenv to install the versions and Poetry or uv to manage the virtual environments.

uv, in particular, is extremely fast and can actually handle the pyenv part (installing Python versions) and the venv part (managing packages) all in one tool.

macOS Python Environment Recovery Guide

  1. The Problem: Conflicting Python Paths

On macOS, multiple Python installations (System, Homebrew, and Manual) often fight for control of the python3 command.

Path Source Recommendation
/usr/bin/python3 macOS System Do Not Touch - Required by OS
/usr/local/bin/python3 Manual Installers Delete Causes major conflicts
/opt/homebrew/bin/python3 Homebrew Use via pyenv. Stable and modern
  1. Cleaning up "Rogue" Installs

To remove manual versions (usually from Python.org) that sit in /usr/local/bin:

# 1. Remove the Application folder
sudo rm -rf /Applications/Python\ 3.12

# 2. Remove the Framework files
sudo rm -rf /Library/Frameworks/Python.framework/Versions/3.12

# 3. Clean up the symlinks in /usr/local/bin
sudo rm /usr/local/bin/python3*
sudo rm /usr/local/bin/pip3*
  1. Setting up pyenv (The "Boss" of Versions)

Instead of hard-coding paths, use pyenv to intercept the python3 command.

  • Installation
brew install pyenv
  • Shell Configuration (~/.zshrc)

Add these lines to ensure pyenv takes priority over everything else:

export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
  • Reload with source ~/.zshrc.

  • Managing Multiple Versions

Each version is isolated. Patch updates (e.g., 3.12.0 to 3.12.9) are treated as new installs.

Basic Commands - Install: pyenv install 3.12.9 - Set Global: pyenv global 3.12.9 - Set per Project: pyenv local 3.11.0 (Creates a .python-version file) - Uninstall: pyenv uninstall 3.12.0

Package Migration Function

Add this to ~/.zshrc to move libraries between versions:

py-migrate() {
 local old_ver=$1
 local new_ver=$2
 pyenv shell $old_ver
 pip freeze > /tmp/migrate_reqs.txt
 pyenv shell $new_ver
 pip install --upgrade pip
 pip install -r /tmp/migrate_reqs.txt
 rm /tmp/migrate_reqs.txt
 }

Usage: py-migrate 3.12.0 3.12.9

  1. Virtual Environments & pyenv

pyenv manages the Python Version. venv manages the Project Libraries.

When you run python -m venv .venv, the environment is "locked" to the pyenv version active at that moment.

Verification Checklist

Run these to ensure your environment is healthy:

  • which python3 -> Should point to ~/.pyenv/shims/python3
  • which -a python3 -> Should show pyenv first, then /usr/bin/python3