This guide is the operational and architectural reference for pyenv — the shell-based Python version manager. It is written for the people who deploy pyenv across machines and CI, harden and tune Python builds, automate provisioning, write plugins, or need to understand exactly what every line of pyenv init does to a shell.
For installation and day-to-day command usage, see the companion User Guide. This document goes deeper: the shim execution model, the hook system, the version-resolution pipeline, the complete build-customization surface, security considerations, and the project's internal structure.
Table of Contents
- Executive Summary
- Architecture
- Repository Structure
- System Requirements
- Filesystem Layout
- Configuration Reference
- Deployment & Provisioning
- Operations
- Security
- Writing Plugins
- Build System & Testing
- Troubleshooting (Operational)
- Potential Improvements
- Quick Reference
Executive Summary
pyenv is a Python interpreter version manager implemented entirely as POSIX shell scripts. It has no compiled core (aside from one optional Bash builtin for speed) and no runtime dependency on Python itself — a deliberate design choice that avoids any bootstrap problem.
Its central mechanism is the shim: a directory of lightweight executables placed at the front of PATH that intercept invocations of python, pip, and every command provided by an installed Python, then redirect them to the version selected by configuration. Selection is layered — shell session, project directory, then global user default — and resolved fresh on every command.
| Attribute | Value |
|---|---|
| Language | Bash / POSIX shell (one optional C Bash builtin in src/) |
| Runtime dependencies | A POSIX shell; readline or GNU coreutils; git for checkout installs |
| Build dependencies | C compiler + Python build libraries (for pyenv install) |
| Supported platforms | Linux, macOS, WSL, other UNIX-like systems (no native Windows) |
| Supported shells | Bash, Zsh, Fish, ksh/ksh93/mksh, PowerShell, Nushell |
| Distribution | Git repo, pyenv.run installer, Homebrew, MacPorts |
| License | MIT |
| Latest release (this snapshot) | v2.7.1 |
| Lineage | Forked from rbenv / ruby-build |
Note
Because pyenv is "pure shell," its attack surface, performance profile, and failure modes are those of shell scripts and the build toolchain — not of a long-running daemon. There is no service to monitor, no port to open, and no privileged process.
Architecture
The dispatcher model
pyenv is a thin front-end (libexec/pyenv) that delegates to subcommands by name, exactly like git. When you run pyenv install, the dispatcher locates an executable named pyenv-install on PATH and execs it.
The dispatcher's startup sequence (libexec/pyenv):
- Handle
--debugby exportingPYENV_DEBUG=1and enablingset -xwith an informativePS4. - Resolve its own install prefix via a
realpath/readlinkabs_dirname(preferring the optionalrealpathBash builtin insrc/when present, else GNUreadlink). - Normalize
PYENV_ROOT(default~/.pyenv) andPYENV_DIR(default$PWD). - Prepend every
plugins/*/bindirectory andlibexectoPATHso subcommands and plugin commands are discoverable. - Build
PYENV_HOOK_PATHfrom a fixed set of locations plus per-pluginetc/pyenv.ddirectories. - Dispatch on the first argument: empty prints version+help,
-v/--versionrunspyenv---version,-h/--helprunspyenv-help, otherwiseexecpyenv-<command>with the remaining args (a trailing--helpis rerouted topyenv-help <command>).
pyenv install 3.12.4
│
├─ libexec/pyenv (dispatcher: sets PYENV_ROOT/PATH/HOOK_PATH)
└─ exec pyenv-install 3.12.4 (resolved from plugins/python-build/bin)
└─ python-build … (downloads, configures, compiles, installs)
Each subcommand is a standalone script carrying a # Summary: and # Usage: header comment (surfaced by pyenv help) and a --complete branch that prints its valid completions/flags (consumed by the shell completion scripts).
The shim mechanism
The shim is the heart of pyenv. After pyenv init places $(pyenv root)/shims at the front of PATH, every Python-provided command name in that directory shadows the real one.
A shim is a copy of this prototype shim (generated by pyenv-rehash):
#!/usr/bin/env bash
set -e
[ -n "$PYENV_DEBUG" ] && set -x
program="${0##*/}"
export PYENV_ROOT="…"
SHIM_PATH=${0%/*}
if [[ $SHIM_PATH != "$PYENV_ROOT/shims" ]]; then
export _PYENV_SHIM_PATH="$SHIM_PATH"
fi
exec "$(command -v pyenv)" exec "$program" "$@"
Every shim is byte-for-byte identical except for its filename. When invoked, the shim simply re-execs pyenv exec <its-own-name> <args>. pyenv exec then resolves the active version, prepends that version's bin/ to PATH, sets PYENV_VERSION, and runs the real executable.
Important
Because shims are identical copies, pyenv detects an upgrade by comparing the first existing shim against the freshly generated prototype. If they differ, all shims are deleted and regenerated. This is why a pyenv upgrade transparently refreshes the shim directory on the next rehash.
The rehash pipeline
pyenv-rehash regenerates the shim directory to match the set of executables across all installed versions. Its design handles concurrency and partial failures carefully:
- Locking. It uses the shell's
noclobberoption to atomically create$(pyenv root)/shims/.pyenv-shimas a lock. Only one rehash runs at a time. A lock file older than 10 minutes is treated as stale and removed. The wait loop honorsPYENV_REHASH_TIMEOUT(default 60s). - Writability probe. Because the Linux Landlock sandbox can make
[ -w ]lie (kernel ≥ 6.14.0), rehash also probes writability by attempting a realmktempinside the shims directory. - Prototype creation. It writes the prototype shim shown above.
- Upgrade detection.
remove_outdated_shimswipes the directory if the existing shims differ from the new prototype. - Registration.
make_shims $(pyenv-versions --executables)registers a shim name for every executable in every installed version. Bash ≥ 4 uses an associative array; Bash 3 falls back to a space-delimited string. - Plugin hooks. Every script returned by
pyenv-hooks rehashis sourced, letting plugins register or suppress shims (see The hook system). - Install & prune. Registered shims are created (hard-link-style copies of the prototype), then any unregistered file in the directory is removed.
Note
The bundled
pyenv.d/rehash/conda.bashhook overridesmake_shimsto skip creating shims for Anaconda-provided system binaries (e.g.openssl,curl) so they don't shadow the user's real tools.pyenv.d/rehash/source.bashrewrites selected shims so that sourced scripts (rather than executed binaries) work correctly.
The hook system
Hooks are the official extension point. The dispatcher assembles PYENV_HOOK_PATH at startup from, in order:
- Any pre-existing
PYENV_HOOK_PATHvalue. $(pyenv root)/pyenv.d.- pyenv's own bundled
pyenv.d(unless pyenv was cloned directly intoPYENV_ROOT). - System paths:
/usr/etc/pyenv.d,/usr/local/etc/pyenv.d,/etc/pyenv.d,/usr/lib/pyenv/hooks. - Each
$(pyenv root)/plugins/*/etc/pyenv.d.
A hook is a .bash file under <hook-path>/<command>/. When a command runs pyenv-hooks <command>, it collects every matching hook script; the command then sources them at a defined point. The bundled hook directories are:
| Hook point | Bundled hooks | Effect |
|---|---|---|
rehash |
conda.bash, source.bash |
Adjust which shims get generated. |
exec |
pip-rehash.bash (+ pip-rehash/ wrappers) |
Auto-rehash after pip/easy_install/conda install commands so new console scripts get shims immediately. |
install |
latest.bash |
Resolve latest-style version requests at install time. |
Tip
Use
pyenv hooks <command>to enumerate exactly which hook scripts are active for a command on a given machine — invaluable when a plugin changes behavior unexpectedly.
Shell integration internals
pyenv init - is the only pyenv command that injects code into your interactive shell. Running eval "$(pyenv init - <shell>)" performs these steps (see libexec/pyenv-init):
- Detect shell (skipped if you pass the shell name — recommended for speed). Detection reads
/proc/$PPID/cmdlineorpsto identify the parent shell. - Create dirs —
mkdir -p $(pyenv root)/{shims,versions}. - Set up the shims path — prepend
$(pyenv root)/shimstoPATH, first stripping any existing occurrence so repeatedinitcalls don't duplicate entries.--no-push-pathguards this with a "skip if already present" check instead. - Export
PYENV_SHELLso subcommands know the active shell. - Source completions for the detected shell from
completions/pyenv.<shell>if readable. - Emit a rehash (
command pyenv rehash) unless--no-rehashis given. - Define the
pyenvshell function, which intercepts the state-mutating subcommands (those returned bypyenv commands --sh, e.g.shell,rehash) andevals their output in the current shell, delegating everything else to the realpyenvbinary.
eval "$(pyenv init - bash)" → full integration (shims + function + completion + rehash)
eval "$(pyenv init --path)" → shims path + rehash only, no shell function/completion
pyenv init --install [shell] → append the setup lines to the shell's startup files
pyenv init --detect-shell → print detected shell + profile/rc file paths
Important
The shell function is what makes
pyenv shell, which must mutate the calling shell'sPYENV_VERSION, possible. Without integration, only the non-mutating subcommands work; pyenv emitsshell integration not enabledforpyenv shell.
pyenv init --install writes to per-shell startup files. For POSIX shells it targets both the rc file (interactive) and the login profile (~/.bash_profile if present else ~/.profile for Bash; ~/.zprofile for Zsh). It refuses to edit any file that already contains the string pyenv (case-insensitive), printing guidance to apply changes manually — a guard against duplicate or conflicting configuration.
Version resolution pipeline
On each shim invocation, pyenv exec (through pyenv-version-name/pyenv-version-file) determines the active version from the first source that resolves:
1. $PYENV_VERSION (set by `pyenv shell`)
2. ./.python-version (current directory)
3. ../.python-version, ../../… (walk up to filesystem root)
4. $(pyenv root)/version (global; absent ⇒ "system")
Multiple whitespace/newline-separated versions in any source are all activated, searched left-to-right; system is always implicitly appended last (fall-through). Prefix auto-resolution applies to all subcommands except uninstall: install resolves a prefix to the latest known version, other commands to the latest installed one. A legacy .pyenv-version file is still honored but a sibling .python-version takes precedence.
Repository Structure
pyenv/
├── bin/ Thin entry stubs (e.g. `pyenv` on PATH)
├── libexec/ Core subcommands — one script per `pyenv-<cmd>`
│ ├── pyenv Dispatcher
│ ├── pyenv-init Shell integration generator
│ ├── pyenv-rehash Shim regeneration
│ ├── pyenv-exec Run a command under the selected version
│ ├── pyenv-version* Resolution + reporting commands
│ ├── pyenv-global/-local Selection persistence
│ ├── pyenv-sh-shell Session selection (shell-function backed)
│ └── … which, whence, prefix, root, latest, hooks, shims, commands, completions, help
├── plugins/
│ └── python-build/ The build engine behind `pyenv install`
│ ├── bin/ pyenv-install, pyenv-uninstall, python-build
│ ├── share/python-build/ Build definitions (one file per version) + patches
│ └── scripts/ Maintenance helpers (add_cpython.py, add_miniconda.py, …)
├── pyenv.d/ Bundled hooks (exec/, rehash/, install/)
├── completions/ pyenv.bash, pyenv.zsh, pyenv.fish, pyenv.pwsh
├── man/man1/pyenv.1 Man page
├── src/ Optional C `realpath` Bash builtin (speed-up)
├── test/ Bats test suite + Dockerfile
├── COMMANDS.md Command reference
├── CHANGELOG.md Release history
└── README.md Primary documentation
Note
The bulk of the 1,500+ files in the repo are build definitions and patches under
plugins/python-build/share/python-build/. Each installable Python version is a small shell definition file; per-version patches live inpatches/<version>/. The executable surface — the part that does the work — is the ~25 scripts inlibexec/pluspython-build.
System Requirements
| Component | Requirement | Notes |
|---|---|---|
| Shell | POSIX-compatible | Bash 3.2+ supported; Bash ≥ 4 enables associative-array fast paths in rehash. |
| Path resolution | readline/GNU coreutils or the compiled src/realpath builtin |
Set PYENV_NATIVE_EXT to force the builtin and fail loudly if unavailable. |
| Version control | git |
For checkout installs and pyenv update. |
| Build toolchain | C compiler + dev headers | OpenSSL, zlib, readline, SQLite, bzip2, libffi, etc. See Suggested build environment. |
| Optional download accelerator | aria2 |
Used automatically if aria2c is on PATH. |
| Optional checksum tools | shasum / openssl / sha256sum |
Enable automatic SHA-2 verification of downloads. |
Filesystem Layout
Everything pyenv manages lives under the pyenv root (PYENV_ROOT, default ~/.pyenv):
| Path | Purpose |
|---|---|
$(pyenv root)/versions/<name>/ |
One directory per installed Python. The directory name is the version name. |
$(pyenv root)/shims/ |
Generated shim executables (front of PATH). |
$(pyenv root)/version |
Global version selection file. |
$(pyenv root)/shims/.pyenv-shim |
Rehash lock file. |
$(pyenv root)/cache/ |
Download cache (if it exists; or PYTHON_BUILD_CACHE_PATH). |
$(pyenv root)/sources/ |
Kept source trees when pyenv install --keep is used. |
$(pyenv root)/plugins/<name>/ |
Installed plugins. |
$(pyenv root)/pyenv.d/ |
User hook scripts. |
./.python-version |
Per-directory selection (project-committable). |
Tip
Because version names are just directory names, you can rename or symlink versions, or back up
versions/directly. A version "exists" to pyenv if and only if its directory is present.
Configuration Reference
Runtime environment variables
These influence pyenv's behavior at command time.
| Variable | Default | Description |
|---|---|---|
PYENV_VERSION |
— | Active version for the current shell session. Set/cleared by pyenv shell. |
PYENV_ROOT |
~/.pyenv |
Root directory for versions, shims, cache, plugins. |
PYENV_DIR |
$PWD |
Directory where .python-version resolution begins. |
PYENV_DEBUG |
— | Enables set -x shell tracing. Equivalent to pyenv --debug <cmd>. |
PYENV_HOOK_PATH |
(assembled) | Colon-separated hook search path (see hooks). |
PYENV_SHELL |
(set by init) | The shell pyenv was initialized for. |
PYENV_REHASH_TIMEOUT |
60 |
Seconds pyenv rehash waits for its lock before giving up. |
PYENV_NATIVE_EXT |
— | If set, require the compiled realpath builtin; abort if it can't load. |
_PYENV_INSTALL_PREFIX |
(computed) | Internal — pyenv's own install directory. |
_PYENV_SHIM_PATH |
(set by shim) | Internal — set when a shim is invoked from outside the canonical shims dir. |
Build customization (python-build)
These variables tune pyenv install / python-build. They are the primary lever for hardening, optimizing, mirroring, and offline-provisioning Python builds.
Build locations & caching
| Variable | Description |
|---|---|
TMPDIR |
Where temporary build files go. |
PYTHON_BUILD_BUILD_PATH |
Where sources are downloaded and built (default: a subdir of TMPDIR). Set this on noexec-restricted machines. |
PYTHON_BUILD_CACHE_PATH |
Directory for caching downloaded archives (default ~/.pyenv/cache if present). Pre-seed it to provision offline. |
PYTHON_BUILD_ROOT |
Override the location of build definitions in share/python-build/. |
PYTHON_BUILD_DEFINITIONS |
Colon-separated extra paths searched for build definitions. |
Download & mirrors
| Variable | Description |
|---|---|
PYTHON_BUILD_MIRROR_URL |
Override the mirror root. URLs are formed as <mirror>/<sha2>. |
PYTHON_BUILD_MIRROR_URL_SKIP_CHECKSUM |
Don't append the checksum to mirror URLs (for pull-through caches like Artifactory). |
PYTHON_BUILD_SKIP_MIRROR |
Always download from the original source URL, never the mirror. |
PYTHON_BUILD_HTTP_CLIENT |
Force aria2, curl, or wget (default: that search order). |
PYTHON_BUILD_CURL_OPTS / PYTHON_BUILD_WGET_OPTS / PYTHON_BUILD_ARIA2_OPTS |
Extra args to the chosen HTTP client. |
Dependency ecosystems (macOS/BSD)
| Variable | Description |
|---|---|
PYTHON_BUILD_USE_HOMEBREW / PYTHON_BUILD_SKIP_HOMEBREW |
Force-enable / disable Homebrew dependency discovery. |
PYTHON_BUILD_USE_MACPORTS / PYTHON_BUILD_SKIP_MACPORTS |
Force-enable / disable MacPorts discovery. |
PYTHON_BUILD_HOMEBREW_OPENSSL_FORMULA |
Override the Homebrew OpenSSL formula. |
PYTHON_BUILD_MACPORTS_OPENSSL_FORMULA |
Override the MacPorts OpenSSL formula (default openssl3 openssl). |
PYTHON_BUILD_TCLTK_FORMULA |
Override the Homebrew Tcl/Tk formula. |
Compiler & make
| Variable | Description |
|---|---|
CC |
Path to the C compiler. |
CONFIGURE_OPTS |
Extra options for ./configure. |
MAKE |
Override the make command (e.g. gmake). |
MAKE_OPTS / MAKEOPTS |
Extra options for make. |
MAKE_INSTALL_OPTS |
Extra options for make install. |
PROFILE_TASK |
Override the PGO profiling task (with --enable-optimizations). |
<PACKAGE>_CFLAGS / _CPPFLAGS / _LDFLAGS |
Per-package compiler flags (e.g. PYTHON_CFLAGS, READLINE_LDFLAGS). |
<PACKAGE>_CONFIGURE_OPTS / _MAKE_OPTS / _MAKE_INSTALL_OPTS / _MAKE_INSTALL_TARGET |
Per-package configure/make controls. |
Note
Default build behavior aims for least surprise but sets a few non-default options:
--enable-sharedis on (required to embed CPython and matches the official Docker image),--with-universal-archsdefaults touniversal2on Apple Silicon, andMACOSX_DEPLOYMENT_TARGETdefaults to the running macOS version. Pass overrides viaCONFIGURE_OPTS/PYTHON_CONFIGURE_OPTS.
Deployment & Provisioning
Reproducible install on a fleet of machines
Pin pyenv to an exact tag and a known PYENV_ROOT:
export PYENV_ROOT=/opt/pyenv
git clone --branch v2.7.1 --depth 1 https://github.com/pyenv/pyenv.git "$PYENV_ROOT"
Then provision shells non-interactively. For automation that must not edit dotfiles, prefer --path and avoid the interactive --install:
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)"
Docker / CI
The recommended pattern is to pin a version at build time and put shims on PATH:
ENV PYENV_ROOT=/opt/pyenv
ENV PATH=$PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH
RUN git clone --branch v2.7.1 --depth 1 https://github.com/pyenv/pyenv.git $PYENV_ROOT \
&& pyenv install 3.12.4 \
&& pyenv global 3.12.4 \
&& pyenv rehash
Tip
For CI agents that need to switch Python mid-job, point
BASH_ENVat a file that runseval "$(pyenv init - bash)"so each step gets shims. Mind theBASH_ENV/.bashrcloop warning when doing so.
Offline / air-gapped builds
- Pre-seed
PYTHON_BUILD_CACHE_PATH(e.g.mkdir -p $PYENV_ROOT/cache) and copy source archives there under the filename pyenv reports in its "Downloading …" message. - Or stand up an internal mirror and set
PYTHON_BUILD_MIRROR_URL(withPYTHON_BUILD_MIRROR_URL_SKIP_CHECKSUMfor pull-through caches).
Operations
| Task | Command |
|---|---|
| Verify install | pyenv --version |
| List installed versions | pyenv versions |
| Install a version | pyenv install <version> (-v for full build log) |
| Force-rebuild a broken version | pyenv install -f <version> |
| Remove a version | pyenv uninstall -f <version> |
| Rebuild shims | pyenv rehash |
| Inspect resolution | pyenv version / pyenv version-origin |
| Inspect a command's target | pyenv which <cmd> |
| List active hooks | pyenv hooks <command> |
| Trace a command | pyenv --debug <command> … |
| Update pyenv | pyenv update (plugin) or git -C $(pyenv root) pull |
Health checks
# Confirm shims are first on PATH
echo "$PATH" | tr ':' '\n' | grep -n shims
# Confirm a selected version resolves correctly
pyenv which python
# Confirm a freshly pip-installed tool got a shim
pip install pytest && pyenv rehash && pyenv which pytest
Security
pyenv has no network service and runs with the user's own privileges. The meaningful security considerations are around how Python is obtained and built.
Caution
Curl-to-shell installation.
curl -fsSL https://pyenv.run | bashexecutes a remote script. For controlled environments, prefer a pinned git checkout (--branch <tag>) or vendor the installer and review it, rather than piping a live URL into a shell.
| Concern | Mitigation |
|---|---|
| Download integrity | python-build verifies the SHA-2 checksum of each archive automatically when shasum/openssl/sha256sum is available. Bundled definitions all carry checksums. Keep a checksum tool installed so verification is never silently skipped. |
| Mirror trust | The default mirror is GitHub Pages, with fallback to the official source URL. If you set PYTHON_BUILD_MIRROR_URL, ensure the mirror is trusted; only set PYTHON_BUILD_MIRROR_URL_SKIP_CHECKSUM when you understand it disables URL-embedded checksum matching. |
| Patch application | --patch reads from stdin and applies to source before build. Review patch sources, especially when fetched over HTTP. |
| PATH precedence | Shims sit at the front of PATH. A malicious package that installs a command name (e.g. a fake python) would get a shim like any other. Audit what you pip install. |
| Startup-file edits | pyenv init --install edits shell startup files but refuses files already containing pyenv config — review diffs in managed environments, or use --path in automation to avoid dotfile edits entirely. |
| Shim PATH leakage | Shims add the selected version's bin/ to child PATH; some downstream tools observe internal paths (#3436). Use pyenv exec scoping or unset PYENV_VERSION for nested shells where this matters. |
Writing Plugins
A pyenv plugin is a directory under $(pyenv root)/plugins/<name>/ that mirrors pyenv's own layout. The mapping is simple: anything pyenv looks for under $(pyenv root)/<path> it also looks for under $(pyenv root)/plugins/<name>/<path>.
| Put under the plugin… | …to provide |
|---|---|
bin/pyenv-<command> |
A new pyenv <command> subcommand (appears in pyenv commands). |
etc/pyenv.d/<hook-point>/*.bash |
A hook that runs at exec, rehash, install, which, version-name, or version-origin. |
share/python-build/<name> |
A custom build definition for pyenv install <name>. |
Authoring a subcommand. Make bin/pyenv-foo executable, add a # Summary:/# Usage: header so pyenv help foo works, and handle a --complete first argument to feed shell completion.
Authoring a hook. A hook is a Bash fragment sourced by the host command — it shares the command's variables and functions and can override them (as conda.bash overrides make_shims). To affect shell state from a hook, it must run within a sh-* command path that the shell function evals.
See Authoring plugins on the wiki for the full hook-point catalog.
Build System & Testing
pyenv's test suite uses Bats, driven by a Makefile.
| Make target | Runs |
|---|---|
make test |
Whole suite on the local host. |
make test-docker |
Whole suite in Docker. |
make test-unit |
Unit tests only. |
make test-plugin |
python-build plugin tests. |
make test-unit-docker-<BASH_VERSION> |
Unit tests under an official Bash container at a pinned version. |
make test-unit-docker-gnu-<BASH_VERSION> |
…plus GNU tools layered in. |
Filter tests with Bats environment variables:
BATS_TEST_FILTER=".*installed.*" BATS_FILE_FILTER="build.bats" make test-plugin
build.bats
✓ yaml is installed for python
✓ homebrew is used in Linux if Pyenv is installed with Homebrew
✓ homebrew is not used in Linux if Pyenv is not installed with Homebrew
3 tests, 0 failures
Run the raw suite directly with Bats:
bats test
bats test/<file>.bats
Note
The optional
src/C extension provides arealpathBash builtin. Build it withsrc/configure && make -C src; it is a pure performance optimization and pyenv falls back to GNUreadlinkif it is absent. Tests run across multiple Bash versions (including 3.2) because pyenv deliberately supports old Bash.
Troubleshooting (Operational)
Symptom: rehash intermittently fails to acquire its lock
Cause: A crashed rehash left .pyenv-shim, or storage is slow. (#3457)
Diagnosis: ls -la $(pyenv root)/shims/.pyenv-shim; check its age.
Resolution: Stale locks older than 10 minutes are auto-removed; force with rm -f $(pyenv root)/shims/.pyenv-shim. Raise PYENV_REHASH_TIMEOUT on slow filesystems. Keep shims on local storage.
Symptom: shims dir reports unwritable on a sandboxed host
Cause: Landlock (Linux ≥ 6.14.0) can make [ -w ] report incorrectly. pyenv compensates with a mktemp probe, but a genuinely read-only shims/ still fails.
Resolution: Ensure the user owns and can write $(pyenv root)/shims; review Landlock/AppArmor policy for the directory.
Symptom: build fails with a missing-module error after a successful compile
Cause: A development header (OpenSSL, SQLite, expat, libffi) was absent at build time, so CPython built without that module. (#3449)
Resolution: Install the dev package, then pyenv install -f <version>. Unset a misleading LD_LIBRARY_PATH. Consult Common build problems.
Symptom: pyenv init causes an infinite loop or slow shells
Cause: BASH_ENV pointing at .bashrc (#264), or repeated detection/rehash at startup (#2918).
Resolution: Put eval "$(pyenv init - bash)" in .bash_profile, not .bashrc, on such systems. Always name the shell, and add --no-rehash to the init line, rehashing manually after installs.
Potential Improvements
Drawn from the project's own open issue tracker, these are areas an operator or contributor may want to track or address:
- Prebuilt CPython distribution (#2334) — would eliminate the build-toolchain requirement for common platforms; today every source release compiles locally.
- XDG base-directory compliance (#1789) —
PYENV_ROOTcurrently centralizes config+data+cache under one tree rather than following$XDG_CONFIG_HOME/$XDG_CACHE_HOME. - Shim startup latency (#2802, #2375) — each shim invocation re-execs pyenv; a hot-path optimization would reduce per-command overhead, especially on macOS.
- Signature verification of CPython artifacts (#3087) — current verification is SHA-2 checksum based; Sigstore verification would strengthen supply-chain assurance.
- Sandbox/Landlock writability detection — the
mktempprobe is a workaround for a kernelaccess()quirk; a cleaner errno-aware check would be more robust.
Note
These reflect open community requests at the documented commit and are not commitments by the project. Verify current status on the issue tracker before relying on any of them.
Quick Reference
Selection precedence (highest first): PYENV_VERSION → ./.python-version → parent .python-version → $(pyenv root)/version → system.
Init modes: pyenv init - (full) · pyenv init --path (shims only) · pyenv init --install (edit dotfiles) · pyenv init --detect-shell.
Key paths: versions $(pyenv root)/versions/<name>/ · shims $(pyenv root)/shims/ · global file $(pyenv root)/version · cache $(pyenv root)/cache/.
Most-used commands:
| Command | Does |
|---|---|
pyenv install -v <v> |
Build & install a version with full log |
pyenv global / local / shell <v> |
Set selection at each scope |
pyenv versions / version |
List / show active |
pyenv rehash |
Rebuild shims |
pyenv which <cmd> |
Resolve a shim |
pyenv exec <cmd> |
Run under selected version, no shims |
pyenv hooks <cmd> |
List active hooks |
pyenv --debug <cmd> |
Trace execution |
Build tuning quick hits: PYTHON_CONFIGURE_OPTS='--enable-optimizations --with-lto' for speed · PYTHON_BUILD_CACHE_PATH for caching · PYTHON_BUILD_MIRROR_URL for an internal mirror · CONFIGURE_OPTS/CC/MAKE_OPTS for toolchain control.