How it works¶
Where does python-discovery look?¶
When you call get_interpreter(), the library checks several locations in
order. It stops as soon as it finds an interpreter that matches your spec.
flowchart TD
Start["get_interpreter()"] --> AbsPath{"Is spec an<br>absolute path?"}
AbsPath -->|Yes| TryAbs["Use path directly"]
AbsPath -->|No| TryFirst["try_first_with paths"]
TryFirst --> RelPath{"Is spec a<br>relative path?"}
RelPath -->|Yes| TryRel["Resolve relative to cwd"]
RelPath -->|No| Current["Current interpreter"]
Current --> Win{"Windows?"}
Win -->|Yes| PEP514["PEP 514 registry"]
Win -->|No| PATH
PEP514 --> PATH["PATH search"]
PATH --> Shims["Version-manager shims<br>(pyenv / mise / asdf)"]
Shims --> UV["uv-managed Pythons"]
TryAbs --> Verify
TryRel --> Verify
UV --> Verify
Verify{{"Verify candidate<br>(subprocess call)"}}
Verify -->|Matches spec| Cache["Cache and return"]
Verify -->|No match| Next["Try next candidate"]
style Start fill:#4a90d9,stroke:#2a5f8f,color:#fff
style Verify fill:#d9904a,stroke:#8f5f2a,color:#fff
style Cache fill:#4a9f4a,stroke:#2a6f2a,color:#fff
style Next fill:#d94a4a,stroke:#8f2a2a,color:#fff
Each candidate is verified by running it as a subprocess and collecting its metadata (version, architecture, platform, sysconfig values, etc.). This subprocess call is the expensive part, which is why results are cached.
How version-manager shims are handled¶
Version managers like pyenv install thin wrapper scripts called
shims (e.g., ~/.pyenv/shims/python3.12) that redirect to the real interpreter. python-discovery
detects these shims and resolves them to the actual binary.
flowchart TD
Shim["Shim detected"] --> EnvVar{"PYENV_VERSION<br>set?"}
EnvVar -->|Yes| Use["Use that version"]
EnvVar -->|No| File{".python-version<br>file exists?"}
File -->|Yes| Use
File -->|No| Global{"pyenv global<br>version exists?"}
Global -->|Yes| Use
Global -->|No| Skip["Skip shim"]
style Shim fill:#4a90d9,stroke:#2a5f8f,color:#fff
style Use fill:#4a9f4a,stroke:#2a6f2a,color:#fff
style Skip fill:#d94a4a,stroke:#8f2a2a,color:#fff
mise and asdf work similarly, using the
MISE_DATA_DIR and ASDF_DATA_DIR environment variables to locate their installations.
How caching works¶
Querying an interpreter requires a subprocess call, which is slow. The cache avoids repeating this work by storing the result as a JSON file keyed by the interpreter’s path.
flowchart TD
Lookup["py_info(path)"] --> Exists{"Cache hit?"}
Exists -->|Yes| Read["Read JSON"]
Exists -->|No| Run["Run subprocess"]
Run --> Write["Write JSON<br>(with filelock)"]
Write --> Return["Return PythonInfo"]
Read --> Return
style Lookup fill:#4a90d9,stroke:#2a5f8f,color:#fff
style Return fill:#4a9f4a,stroke:#2a6f2a,color:#fff
style Run fill:#d9904a,stroke:#8f5f2a,color:#fff
The built-in DiskCache stores files under <root>/py_info/4/<sha256>.json
with filelock-based locking for safe concurrent access. You
can also pass cache=None to disable caching, or implement your own backend (see
How-to guides).
Subprocess timeout behavior¶
When python-discovery verifies an interpreter candidate, it runs a subprocess to query its metadata. On slow systems (especially Windows), Python startup can take significant time. The default timeout is 15 seconds to balance responsiveness with accommodation for real-world conditions.
If your system consistently hits timeouts, you can customize the timeout via the
PY_DISCOVERY_TIMEOUT environment variable (in seconds):
# Increase timeout to 30 seconds
export PY_DISCOVERY_TIMEOUT=30
python -c "from python_discovery import get_interpreter; get_interpreter('python3.12')"
The timeout applies to each individual interpreter being queried. If you set a value that is too low, legitimate interpreters may be skipped; if too high, the discovery process may take longer to fail when encountering problematic interpreters.
Spec format reference¶
A spec string follows the pattern [impl][version][t][-arch][-machine]. Every part is optional.
flowchart TD
Spec["Spec string"] --> Impl["impl<br>(optional)"]
Impl --> Version["version<br>(optional)"]
Version --> T["t<br>(optional)"]
T --> Arch["-arch<br>(optional)"]
Arch --> Machine["-machine<br>(optional)"]
style Impl fill:#4a90d9,stroke:#2a5f8f,color:#fff
style Version fill:#4a9f4a,stroke:#2a6f2a,color:#fff
style T fill:#d9904a,stroke:#8f5f2a,color:#fff
style Arch fill:#d94a4a,stroke:#8f2a2a,color:#fff
style Machine fill:#904ad9,stroke:#5f2a8f,color:#fff
Parts explained:
impl – the Python implementation name.
pythonandpyboth mean “any implementation” (usually CPython). Usecpython,pypy, orgraalpyto be explicit.version – dotted version number (
3,3.12, or3.12.1). You can also write312as shorthand for3.12.t – appended directly after the version. Matches free-threaded (no-GIL) builds only.
-arch –
-32or-64for 32-bit or 64-bit interpreters.-machine – the CPU instruction set:
-arm64,-x86_64,-aarch64,-riscv64, etc.
Full examples:
Spec |
Meaning |
|---|---|
|
Any Python 3.12 |
|
CPython 3.12 |
|
Explicitly CPython 3.12 |
|
PyPy 3.9 |
|
Free-threaded (no-GIL) CPython 3.13 |
|
64-bit CPython 3.12 |
|
64-bit CPython 3.12 on ARM64 |
|
Absolute path, used directly (no search) |
|
PEP 440 version specifier (any Python in range) |
|
PEP 440 specifier restricted to CPython |
PEP 440 specifiers (>=, <=, ~=, !=, ==, ===) are supported. Multiple
specifiers can be comma-separated, for example >=3.11,<3.13.