Pyenv solves a fundamental mismatch between operating systems and Python development: Linux and macOS ship a system Python that the OS uses internally, which must never be modified, but every Python project needs a specific interpreter version—pyenv resolves this by intercepting every python invocation via a shim before the OS ever sees it.
<svg viewBox="0 0 760 470" xmlns="http://www.w3.org/2000/svg" font-family="monospace" font-size="12">
<rect width="760" height="470" fill="#0d1117"/>
<text x="380" y="30" fill="#e6edf3" font-size="13" text-anchor="middle" font-weight="bold">pyenv Version Resolution: shim → .python-version → global</text>
<!-- Shell command box -->
<rect x="30" y="55" width="200" height="38" fill="#161b22" rx="4" stroke="#388bfd" stroke-width="1.5"/>
<text x="130" y="70" fill="#388bfd" font-size="11" text-anchor="middle">$ python script.py</text>
<text x="130" y="84" fill="#8b949e" font-size="10" text-anchor="middle">PATH finds ~/.pyenv/shims/python</text>
<!-- Arrow down -->
<line x1="130" y1="93" x2="130" y2="118" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/>
<!-- Shim box -->
<rect x="30" y="118" width="200" height="50" fill="#161b22" rx="4" stroke="#e3b341" stroke-width="1.5"/>
<text x="130" y="135" fill="#e3b341" font-size="11" text-anchor="middle">~/.pyenv/shims/python</text>
<text x="130" y="149" fill="#8b949e" font-size="10" text-anchor="middle">3-line bash script</text>
<text x="130" y="162" fill="#8b949e" font-size="10" text-anchor="middle">~20 ms overhead</text>
<!-- Arrow down to resolver -->
<line x1="130" y1="168" x2="130" y2="193" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/>
<!-- Resolver box -->
<rect x="30" y="193" width="200" height="38" fill="#161b22" rx="4" stroke="#8b949e" stroke-width="1"/>
<text x="130" y="208" fill="#e6edf3" font-size="11" text-anchor="middle">pyenv exec (resolver)</text>
<text x="130" y="222" fill="#8b949e" font-size="10" text-anchor="middle">checks 3 sources in order</text>
<!-- Three branches from resolver -->
<!-- Branch 1: PYENV_VERSION env var -->
<line x1="130" y1="231" x2="130" y2="252" stroke="#8b949e" stroke-width="1"/>
<line x1="130" y1="252" x2="50" y2="252" stroke="#8b949e" stroke-width="1"/>
<line x1="50" y1="252" x2="50" y2="268" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/>
<rect x="10" y="268" width="80" height="40" fill="#161b22" rx="3" stroke="#3fb950" stroke-width="1.5"/>
<text x="50" y="281" fill="#3fb950" font-size="9" text-anchor="middle">PYENV_VERSION</text>
<text x="50" y="294" fill="#3fb950" font-size="9" text-anchor="middle">env var</text>
<text x="50" y="307" fill="#8b949e" font-size="8" text-anchor="middle">highest priority</text>
<!-- Branch 2: .python-version file -->
<line x1="130" y1="252" x2="130" y2="268" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/>
<rect x="90" y="268" width="80" height="50" fill="#161b22" rx="3" stroke="#e3b341" stroke-width="1.5"/>
<text x="130" y="281" fill="#e3b341" font-size="9" text-anchor="middle">.python-version</text>
<text x="130" y="293" fill="#e3b341" font-size="9" text-anchor="middle">file (cwd→/)</text>
<text x="130" y="305" fill="#8b949e" font-size="8" text-anchor="middle">stat each dir ~1µs</text>
<text x="130" y="317" fill="#8b949e" font-size="8" text-anchor="middle">per-project</text>
<!-- Branch 3: global -->
<line x1="130" y1="252" x2="210" y2="252" stroke="#8b949e" stroke-width="1"/>
<line x1="210" y1="252" x2="210" y2="268" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/>
<rect x="170" y="268" width="80" height="40" fill="#161b22" rx="3" stroke="#f85149" stroke-width="1.5"/>
<text x="210" y="281" fill="#f85149" font-size="9" text-anchor="middle">~/.pyenv/version</text>
<text x="210" y="294" fill="#f85149" font-size="9" text-anchor="middle">(global fallback)</text>
<!-- All branches converge to versions/ -->
<line x1="50" y1="308" x2="50" y2="360" stroke="#8b949e" stroke-width="1"/>
<line x1="130" y1="318" x2="130" y2="360" stroke="#8b949e" stroke-width="1"/>
<line x1="210" y1="308" x2="210" y2="360" stroke="#8b949e" stroke-width="1"/>
<line x1="50" y1="360" x2="210" y2="360" stroke="#8b949e" stroke-width="1"/>
<line x1="130" y1="360" x2="130" y2="375" stroke="#8b949e" stroke-width="1.5" marker-end="url(#arr)"/>
<rect x="60" y="375" width="140" height="38" fill="#161b22" rx="4" stroke="#388bfd" stroke-width="1.5"/>
<text x="130" y="390" fill="#388bfd" font-size="11" text-anchor="middle">~/.pyenv/versions/</text>
<text x="130" y="403" fill="#8b949e" font-size="10" text-anchor="middle">3.11.5/bin/python3.11</text>
<!-- Right panel: versions directory tree -->
<rect x="290" y="55" width="200" height="200" fill="#161b22" rx="4" stroke="#21262d" stroke-width="1"/>
<text x="310" y="73" fill="#e6edf3" font-size="11" font-weight="bold">~/.pyenv/versions/</text>
<text x="318" y="92" fill="#388bfd" font-size="10">3.9.18/</text>
<text x="326" y="106" fill="#8b949e" font-size="10">bin/ lib/ include/</text>
<text x="326" y="119" fill="#8b949e" font-size="10">~150–300 MB</text>
<text x="318" y="137" fill="#3fb950" font-size="10">3.11.5/</text>
<text x="326" y="151" fill="#8b949e" font-size="10">bin/ lib/ include/</text>
<text x="326" y="164" fill="#8b949e" font-size="10">~150–300 MB</text>
<text x="318" y="182" fill="#e3b341" font-size="10">3.12.0/</text>
<text x="326" y="196" fill="#8b949e" font-size="10">bin/ lib/ include/</text>
<text x="326" y="209" fill="#8b949e" font-size="10">~150–300 MB</text>
<text x="310" y="240" fill="#f85149" font-size="10">700+ versions available</text>
<!-- Right panel: install cost -->
<rect x="290" y="270" width="200" height="145" fill="#161b22" rx="4" stroke="#21262d" stroke-width="1"/>
<text x="310" y="288" fill="#e6edf3" font-size="11" font-weight="bold">Install cost comparison</text>
<text x="310" y="306" fill="#f85149" font-size="10">pyenv compile: 2–5 min</text>
<text x="310" y="320" fill="#8b949e" font-size="9">(gcc from source)</text>
<text x="310" y="340" fill="#388bfd" font-size="10">uv python: 5–30 s</text>
<text x="310" y="354" fill="#8b949e" font-size="9">(pre-built binary)</text>
<text x="310" y="374" fill="#e3b341" font-size="10">mise activate: ~1 ms</text>
<text x="310" y="388" fill="#8b949e" font-size="9">(Rust, no bash shim)</text>
<text x="310" y="405" fill="#8b949e" font-size="9">vs pyenv shim: ~20 ms</text>
<!-- Far right panel: uv comparison -->
<rect x="510" y="55" width="220" height="358" fill="#161b22" rx="4" stroke="#21262d" stroke-width="1"/>
<text x="530" y="73" fill="#e6edf3" font-size="11" font-weight="bold">Shim overhead per python call</text>
<text x="530" y="95" fill="#f85149" font-size="10">pyenv shim: ~20 ms</text>
<rect x="530" y="100" width="120" height="12" fill="#f85149" fill-opacity="0.3" rx="2"/>
<rect x="530" y="100" width="120" height="12" fill="#f85149" rx="2"/>
<text x="530" y="130" fill="#e3b341" font-size="10">asdf: ~15 ms</text>
<rect x="530" y="135" width="90" height="12" fill="#e3b341" rx="2"/>
<text x="530" y="165" fill="#3fb950" font-size="10">mise: ~1 ms</text>
<rect x="530" y="170" width="6" height="12" fill="#3fb950" rx="2"/>
<text x="530" y="200" fill="#388bfd" font-size="10">uv: <1 ms</text>
<rect x="530" y="205" width="3" height="12" fill="#388bfd" rx="2"/>
<text x="530" y="234" fill="#8b949e" font-size="10">0ms 10ms 20ms</text>
<text x="530" y="260" fill="#e6edf3" font-size="10" font-weight="bold">pyenv rehash</text>
<text x="530" y="278" fill="#8b949e" font-size="10">Rebuilds all shims after:</text>
<text x="530" y="294" fill="#8b949e" font-size="10">- New Python version install</text>
<text x="530" y="310" fill="#8b949e" font-size="10">- pip install of CLI tool</text>
<text x="530" y="326" fill="#8b949e" font-size="10">Cost: ~100 ms</text>
<text x="530" y="350" fill="#e6edf3" font-size="10" font-weight="bold">Storage: 3 versions</text>
<text x="530" y="368" fill="#8b949e" font-size="10">3 × 250 MB = 750 MB</text>
<text x="530" y="384" fill="#8b949e" font-size="10">+ stdlib + site-packages</text>
<text x="530" y="400" fill="#8b949e" font-size="10">per environment</text>
<!-- Arrow markers -->
<defs>
<marker id="arr" markerWidth="6" markerHeight="6" refX="3" refY="3" orient="auto">
<path d="M0,0 L6,3 L0,6 Z" fill="#8b949e"/>
</marker>
</defs>
</svg>
Pyenv works by inserting ~/.pyenv/shims at the front of PATH and populating that directory with thin bash wrapper scripts—one per Python executable—so that typing python, python3, pip, or ipython always routes through pyenv's resolver, which reads a three-level version stack (PYENV_VERSION environment variable, then the nearest .python-version file, then the global default) before exec'ing the real binary from ~/.pyenv/versions/. The shim for python is literally a 3-line bash script: exec pyenv exec "$0" "$@"—it resolves in ~20 ms because spawning a bash subprocess and running pyenv exec carries unavoidable process-launch overhead. The version lookup itself is fast: scanning directories for .python-version costs ~1 µs per stat() call, and projects typically nest 3–8 directory levels deep.
Installing a Python version via pyenv install downloads CPython source and compiles it from scratch using gcc, taking 2–5 min on a modern 8-core machine and requiring build dependencies (gcc, make, zlib, bzip2, readline, sqlite3, openssl, libffi, lzma) that must be present before the install—a requirement that surprises newcomers on a fresh macOS or Linux system and is the most common cause of pyenv install failures. The compiled interpreter lives at ~/.pyenv/versions/3.11.5/ and occupies ~150–300 MB including the standard library. Storing three Python versions costs ~450–900 MB before any site-packages are installed. The openssl dependency is the most fragile: a mismatch between the system openssl headers and the version pyenv used at compile time causes ssl module import failures, typically resolved by reinstalling with CPPFLAGS and LDFLAGS pointing at the correct openssl path (common on macOS with Homebrew openssl@3).
The .python-version file—a single line containing a version string—is pyenv's mechanism for project-local version specification: pyenv searches for this file from the current directory up toward the root, stopping at the first match, so a file at ~/projects/myapp/.python-version containing "3.11.5" activates that version for every command run inside myapp/ without any shell configuration change. pyenv local 3.11.5 writes this file; pyenv global 3.10.4 writes ~/.pyenv/version as the interpreter-of-last-resort fallback for directories with no .python-version file. pyenv shell 3.12.0 sets PYENV_VERSION in the current shell session only—highest priority, cleared on shell exit. The three-level stack mirrors the scope of the decision: temporary (shell), project-permanent (local), machine-permanent (global).
Pyenv-virtualenv is a pyenv plugin that creates virtual environments stored inside ~/.pyenv/versions/X.Y.Z/envs/ and registers them as pseudo-version names, enabling pyenv local myapp-env to activate both the correct Python version and the correct site-packages directory with a single .python-version entry. Without pyenv-virtualenv, the workflow requires two tools: pyenv selects the interpreter, then python -m venv creates the environment, then the environment must be activated separately. Pyenv-virtualenv merges these: pyenv virtualenv 3.11.5 myapp creates the venv, and pyenv local myapp makes both active in that directory. The runtime behavior is identical to a plain venv—the same site-packages isolation mechanism—but the storage and activation are managed by pyenv's shim layer.
Pyenv rehash rebuilds all shims by scanning every binary in every ~/.pyenv/versions/
Uv's python management subsystem—uv python install, uv python pin—provides a drop-in replacement for pyenv's install and local workflows: it downloads a pre-built CPython binary from the Astral-maintained python-build-standalone project in 5–30 s versus pyenv's 2–5 min compile, writes a .python-version file compatible with pyenv's format, and uses a Rust binary instead of bash shims, reducing per-invocation overhead from ~20 ms to under 1 ms. Mise (formerly rtx) offers the same Rust-speed advantage while supporting all pyenv-style semantics plus asdf plugin compatibility for non-Python runtimes; its activate hook costs ~1 ms versus pyenv's 10–20 ms shell function setup, a difference measurable in CI pipeline startup time across hundreds of jobs. Both tools read pyenv's .python-version format, making migration transparent.
| Tool | Install method | Install time | Per-call overhead | Multi-lang | Lockfile |
|---|---|---|---|---|---|
| pyenv | Compile from source | 2–5 min | ~20 ms (bash shim) | No | .python-version |
| uv python | Pre-built binary | 5–30 s | <1 ms (Rust) | No | .python-version |
| mise | Pre-built binary | 5–30 s | ~1 ms (Rust) | Yes (asdf plugins) | .tool-versions |
| asdf | Plugin (compile/bin) | varies | ~15 ms (bash) | Yes | .tool-versions |
[PYENV VERSION RESOLUTION — per python invocation]
$ python script.py (in ~/projects/myapp/)
|
PATH → ~/.pyenv/shims/python (~20 ms bash overhead)
|
pyenv exec resolves version (priority order):
|
1. PYENV_VERSION=3.12.0 set? → use 3.12.0 (shell-scoped)
|
2. .python-version file found? (stat cwd, parent, ..., /):
| ~/projects/myapp/.python-version → "3.11.5" (per-project)
| ~/.python-version → not checked (stopped at match)
|
3. ~/.pyenv/version → "3.10.4" (global fallback)
|
exec ~/.pyenv/versions/3.11.5/bin/python3.11 script.py
(real interpreter, no further pyenv involvement)
Read pyenv through a PATH interception lens rather than a Python version installer lens: pyenv's core contribution is not downloading Python—any package manager can do that—it is the shim layer that makes version selection transparent to every tool, script, and subprocess that runs python without explicit path qualification. The shim directory is a controlled namespace that sits at the front of PATH and forwards every invocation through a resolver that knows which Python version the current directory's maintainer intended. The compile-from-source model, the .python-version file, and the three-tier priority stack are all consequences of this core design: you need a file to record the intent, a resolver to find and honor it, and a compiled interpreter to actually run it.
Explore 500+ Semiconductor & AI Topics
From EUV lithography to CUDA optimization — search the full knowledge base or chat with our AI assistant.