If you do Python work for more than a few months, you'll eventually need two or more Pythons on the same machine: one project requires 3.10 for a legacy dep, another wants 3.13 for the newest features, and you'd really like the OS Python to stay where it is. There are six tools that solve this in 2026, the right pick depends on your starting point, and most pain comes from not understanding the shim mechanism that powers the older ones. This guide compares all six side-by-side, explains how shims actually intercept the python command, covers per-project pinning with .python-version, and shows how to clean up old Pythons without breaking your OS. It's one of 10 explainers in our Python Environment Setup pillar page.
Key Takeaways
uv is the 2026 default for new setups. Installs and manages multiple Pythons + adds fast package management; replaces pyenv for most users.
pyenv still works. If you already have it set up, no urgency to migrate. Shim-based, mature, large community.
Windows has py launcher built-in. Use py -3.13 and py -3.10; no extra install needed.
Don't rely on PATH order to pick a Python. Use a version manager (uv, pyenv, py) or you'll fight PATH every time you install or update.
Pin the version per project. A .python-version file in the project root tells uv, pyenv, and py launcher which Python to use, automatically.
Six tools, three install patterns (user home, shims, native), and the per-tool commands to install and switch.
Why You Need This
The clean version of the problem: each Python project should run on a specific Python version that matches its tests and deployment target. Forcing every project onto the system Python or onto a single "default" Python ends one of three ways:
You break the OS. System tools on macOS and Linux depend on a specific Python version. Upgrading or replacing the system one occasionally breaks apt, dnf, or macOS framework tools.
You break the project. You upgrade Python from 3.10 to 3.13 and three packages stop working because they don't have wheels yet, or because the project used a removed stdlib feature.
You get stuck on old Python. A project pinned to a legacy Python freezes the rest of your environment in time.
The answer to all three is the same: have multiple Pythons installed, isolated from each other and from the OS, and switch between them per-project. That's what version managers do.
How Shims Work (The Mechanism Behind pyenv and Most Others)
Without shims, the python command runs whatever Python is first on your PATH. That's brittle: install or update anything, and the order changes. Shims solve this by inserting a tiny wrapper script in front of every Python executable.
# What's actually in ~/.pyenv/shims/python:
#!/usr/bin/env bash
set -e
[ -n "$PYENV_DEBUG" ] && set -x
program="${0##*/}"
if [[ "$program" = "python"* ]]; then
for arg; do
case "$arg" in
-c* | -- ) break ;;
*/* )
if [ -f "$arg" ]; then
export PYENV_FILE_ARG="$arg"
break
fi
;;
esac
done
fi
export PYENV_ROOT="/Users/jesse/.pyenv"
exec "/usr/local/bin/pyenv" exec "$program" "$@"
When you type python, the shell finds ~/.pyenv/shims/python first because pyenv put that directory at the front of PATH. The shim runs pyenv exec python with your arguments, which figures out the active version (from PYENV_VERSION, the nearest .python-version file, or the global default) and execs the real Python from ~/.pyenv/versions/3.X.Y/bin/python.
The whole mechanism is invisible day-to-day. You type python, the right one runs. The downside: a layer of indirection that occasionally surprises you (when a tool resolves "the Python it was installed with" by reading sys.executable, the shim path can confuse it).
uv: The 2026 Default
uv took a different approach: instead of shims, it ships standalone Python builds (cpython.org-style binaries managed by Astral) and installs them per-user. Switching is explicit per command (uv run --python 3.13 app.py) or per directory (.python-version file).
# Install uv (one-line bootstrap):
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install Python versions:
uv python install 3.10
uv python install 3.13
uv python install 3.14
uv python list # show what uv knows about
# Per-project pinning:
cd myproject
uv python pin 3.13 # writes .python-version
uv run app.py # runs with Python 3.13 automatically
# Ad-hoc run with a specific Python:
uv run --python 3.10 legacy_script.py
# Uninstall:
uv python uninstall 3.10
The Python binaries uv installs are managed by Astral, isolated from the system, and include pip in the same install. See the uv Python concepts doc for the full feature set.
pyenv has run multi-Python setups for a decade. It's mature, well-documented, and integrates with every Linux shell and Homebrew on macOS. If you already have pyenv working, no reason to migrate.
# Install pyenv (one of these):
curl https://pyenv.run | bash # universal installer
brew install pyenv # macOS Homebrew
# Add to shell rc (e.g., ~/.bashrc, ~/.zshrc):
export PYENV_ROOT="$HOME/.pyenv"
command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
# Install and switch versions:
pyenv install --list | grep " 3.13" # show available
pyenv install 3.13.1
pyenv global 3.13.1 # default everywhere
cd myproject
pyenv local 3.10.13 # override in THIS directory (writes .python-version)
pyenv version # show active
pyenv versions # show installed
pyenv uninstall 3.10.13 # clean removal
Pyenv builds Python from source by default (unless you pass --list-all to find prebuilt versions or use environment variables to fetch binaries). That means installing a Python takes a few minutes the first time; uv's prebuilt binaries are much faster.
py Launcher: The Windows Native
If you're on Windows and only need to switch between installed Pythons (not install new ones), the py launcher is already there. It ships with every modern Python install on Windows; no extra tool needed.
# See what py knows about:
py --list
# -V:3.14 Python 3.14 (64-bit)
# -V:3.13 * Python 3.13 (64-bit)
# -V:3.10 Python 3.10 (64-bit)
# Run a specific version:
py -3.13 script.py
py -3.10 -m pip install requests
py -3.13 -m venv .venv
# .python-version file works too (recent versions of py)
echo "3.13" > .python-version
py script.py # uses 3.13
For installing new Pythons, pair py with the new Python Install Manager (pymanager) that ships with Python 3.14. See our Windows pip install guide for the install side.
asdf and mise: The Multi-Language Managers
If you also juggle Node, Ruby, or other language versions, a polyglot version manager is convenient.
asdf
asdf uses a plugin system for each language. Plugins for Python, Node, Ruby, Go, Java, Rust, and dozens more.
asdf plugin add python # one-time per language
asdf install python 3.13.1
asdf local python 3.13.1 # writes .tool-versions in current dir
asdf global python 3.13.1 # default
asdf list python # show installed
mise
mise is a Rust rewrite of asdf with faster startup and a slightly different config format.
mise use python@3.13 # install + activate
mise use python@3.13 --global
mise ls # show all managed tools
# Per-project: .tool-versions OR .mise.toml
Both are good options if you want one tool for every language on your system. For Python-only setups, uv or pyenv have less overhead.
conda: A Different Paradigm
conda is a package manager that ALSO manages Python versions per environment. Each conda env contains its own Python; switching envs switches Python.
conda create -n datasci python=3.13
conda create -n legacy python=3.10
conda activate datasci # python now 3.13
conda activate legacy # python now 3.10
conda env list
conda's domain is scientific Python where you need native deps (CUDA-linked PyTorch, GDAL, R interop). For pure-Python projects, conda is heavier than uv or pyenv. See our upcoming conda vs pip vs uv guide for when conda is genuinely the right choice.
The macOS Trap: Homebrew, /usr/bin/python3, and pyenv
macOS has three potential Pythons fighting for your PATH:
/usr/bin/python3 — the system Python (Apple-maintained, used by OS tools).
Homebrew Python — typically at /opt/homebrew/bin/python3 (Apple Silicon) or /usr/local/bin/python3 (Intel Macs).
pyenv (or uv) Pythons — at ~/.pyenv/shims/python or ~/.local/share/uv/python/.
If you install Python via Homebrew AND set up pyenv, the order in your shell rc determines which one wins. The cleanest setup: stop using Homebrew Python for development, let pyenv (or uv) own the user-Python story, and leave /usr/bin/python3 alone for the OS.
# Check what owns the python command right now:
which python
which python3
echo $PATH
# Expected output for a clean pyenv setup on macOS:
# /Users/<you>/.pyenv/shims/python
# /Users/<you>/.pyenv/shims/python3
Do not delete /usr/bin/python3. macOS depends on it. The Apple Python is sandboxed enough that pip can't write to it (you get the externally-managed-environment error; see our PEP 668 guide) but the binary needs to stay.
Per-Project Pinning: .python-version
Every modern version manager reads a file named .python-version at the project root:
echo "3.13.1" > .python-version
# Then:
cd myproject
python --version # 3.13.1 (because the file says so)
cd /tmp
python --version # whatever your global default is
Tools that read it: pyenv, uv (since v0.1), py launcher (recent versions), asdf (via .tool-versions with slightly different format).
Commit .python-version to your repo. Collaborators get the same Python automatically. CI systems that use uv or actions/setup-python read it too.
The .tool-versions Alternative
asdf and mise use .tool-versions, which can hold multiple languages:
If your project is polyglot, this is cleaner than one file per language.
Diagnostic: Which Python Am I Actually Running?
Five commands that answer the question definitively:
# 1. Which python on PATH?
which python
type python # bash/zsh, shows shim or alias
where python # Windows
# 2. Where does Python think it's running from?
python -c "import sys; print(sys.executable)"
# 3. Full Python version and platform:
python -c "import sys; print(sys.version)"
# 4. What's pyenv's view?
pyenv version # if you have pyenv
pyenv versions # all installed
# 5. What's uv's view?
uv python list # if you have uv
The most common discrepancy: which python says one thing, sys.executable says another. That's the shim layer in action; the shim is at the PATH location, but it execs the real Python from somewhere else.
Cleaning Up Old Pythons
Old Python versions accumulate. Each takes 100-300 MB. Cleaning them is safe IF you remove them through the tool that installed them.
How Python was installed
How to remove
pyenv
pyenv uninstall 3.X.Y
uv
uv python uninstall 3.X
Homebrew (macOS)
brew uninstall python@3.X
python.org MSI (Windows)
Settings → Add or Remove Programs
pymanager (Windows)
pymanager uninstall 3.X
Microsoft Store
Settings → Apps → Installed apps
conda env
conda env remove -n <name>
asdf / mise
asdf uninstall python 3.X.Y or mise uninstall python@3.X
System (/usr/bin/python3)
DO NOT remove. OS depends on it.
Before removing a Python, check what's installed in it with pip list (run via that specific Python). If you have tools or scripts that target that version, document them or transfer them to a Python you're keeping.
You're a tinkerer who wants the simplest mental model
uv
Common Bugs and Their Causes
"python --version says 3.10 but I just installed 3.13"
PATH ordering. Run which python to see which one is winning. Either rearrange PATH so the new Python comes first, or (better) use a version manager so PATH ordering doesn't matter.
"pyenv installed Python but python still runs the old version"
You forgot the eval "$(pyenv init -)" line in your shell rc, or the shell hasn't reloaded the rc. Open a new terminal or run source ~/.zshrc (or equivalent).
"My script breaks because sys.executable is a shim path"
Some tools (notably old IDEs, some PyInstaller setups) don't handle the shim path well. Either resolve the shim to its real path (readlink -f $(which python)) or switch to uv, which uses real paths everywhere.
"I have pyenv AND uv and python is confused"
Pick one. Don't run both as the primary version manager. Either remove pyenv (rm -rf ~/.pyenv + remove the shell rc lines) or stop using uv's Python management (only use uv for packages).
Frequently Asked Questions
What is the best way to manage multiple Python versions in 2026?
For new setups, uv is the 2026 default: it installs and manages multiple Pythons in your user directory, switches between them per-project via .python-version, and adds a fast package manager in the same binary. For existing pyenv users with a working setup, no urgency to migrate; pyenv still does the job well. Windows users have the py launcher built into Python itself, which handles version selection without extra installs. The wrong answer in most cases is to install multiple Pythons from python.org and rely on PATH order to pick one; PATH-based selection breaks the moment something changes.
How do pyenv shims work?
pyenv installs small shell-script wrappers called shims into ~/.pyenv/shims/ for every Python executable (python, python3, pip, pytest, ruff, etc.). It prepends that directory to your PATH so the shims intercept every call. When you run python, the shim looks at three places in order to decide which actual Python to use: PYENV_VERSION environment variable, the nearest .python-version file walking up the directory tree, and the global pyenv version. Then it execs the real Python from ~/.pyenv/versions/. The whole mechanism is transparent: you type python, pyenv figures out which one.
What is the .python-version file?
.python-version is a plain text file at the root of your project containing the Python version number (for example: 3.13.1). Both pyenv and uv read this file when they run inside that directory and automatically use the requested Python. It's the standard way to declare "this project needs THIS Python" so collaborators get a consistent setup. Commit it to your repository alongside requirements.txt or pyproject.toml. The py launcher on Windows also reads .python-version in recent versions.
Can I use pyenv and uv together?
Yes, but it's usually unnecessary and can cause shim collisions. uv installs its own Pythons to ~/.local/share/uv/python/ and doesn't use pyenv shims; if your PATH has both ~/.pyenv/shims/ and uv's bin directory, the order decides which python wins. The clean answer is to pick one. If you're starting fresh, uv replaces pyenv for the version-management job and adds package management in the same tool. If you already have pyenv and it works, stick with it. Don't try to run them in parallel as a "best of both worlds" setup; that's where strange bugs come from.
How do I uninstall old Python versions safely?
Depends on how they were installed. For pyenv-managed Pythons, pyenv uninstall 3.X.Y removes a version cleanly. For uv-managed, uv python uninstall 3.X. For Homebrew, brew uninstall python@3.X. For Windows python.org MSI installs, use Add or Remove Programs. NEVER delete the system Python on macOS (in /usr/bin/python3) or Linux (in /usr/bin/python3) by hand; OS tools depend on it. Before uninstalling, check pip list inside the Python you're removing to see if anything important lives there; if so, document it before the version is gone.
The Bottom Line: One Tool, .python-version, Don't Touch /usr/bin
Pick a version manager (uv for new, pyenv if you have it). Pin every project with a .python-version file. Never count on PATH order to pick the right Python. Leave the system Python alone. Those four rules cover 95% of multi-Python setups without drama. For everything that comes after switching versions, see our venv decision guide, ModuleNotFoundError fix, or browse the full Python Environment Setup pillar page.
Skip the Version-Manager Decision Tree
CodeGym's Python track runs Python directly in your browser. No pyenv, no uv, no PATH ordering. Pick a lesson, write code, the AI validator grades it. 800+ hands-on tasks across 62 levels and an AI mentor for when an error stumps you. First level free; full plan on the pricing page.
Learn Python on the free track →
GO TO FULL VERSION