Virtual Environments with virtualenv and pyenv¶
DEPRECATED - replaced with:
Using Python with Virtual Environments
Original Content¶
Creating a virtual environment¶
Activate Virtual Environment¶
Linux and macOS - Activate¶
Windows - Activate¶
Using PowerShell:
Deactivate Virtual Environment¶
Linux and macOS - Deactivate¶
Windows - 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.
- 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:
- 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.
- 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:
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
- 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).
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.
- How pyenv handles "Versions"
pyenv keeps every version in its own unique folder inside ~/.env/versions/. Because of this:
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.
- The Recommended Workflow
Instead of overwriting, simply install the latest patch and update your "Global" pointer.
- Check for the latest available 3.12 patch:
- Install the newest one (e.g., 3.12.9):
- Update your Global version:
- Optional: Remove the old one
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.
- 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
- 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
- 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:
Usage:
To migrate from 3.12.0 to 3.12.9, you simply run:
- 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).
- 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.
- 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
- 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¶
- 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 |
- 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*
- Setting up pyenv (The "Boss" of Versions)
Instead of hard-coding paths, use pyenv to intercept the python3 command.
- Installation
- 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
- 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/python3which -a python3-> Should showpyenvfirst, then/usr/bin/python3