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. python and py both mean “any implementation” (usually CPython). Use cpython, pypy, or graalpy to be explicit.

  • version – dotted version number (3, 3.12, or 3.12.1). You can also write 312 as shorthand for 3.12.

  • t – appended directly after the version. Matches free-threaded (no-GIL) builds only.

  • -arch-32 or -64 for 32-bit or 64-bit interpreters.

  • -machine – the CPU instruction set: -arm64, -x86_64, -aarch64, -riscv64, etc.

Full examples:

Spec

Meaning

3.12

Any Python 3.12

python3.12

CPython 3.12

cpython3.12

Explicitly CPython 3.12

pypy3.9

PyPy 3.9

python3.13t

Free-threaded (no-GIL) CPython 3.13

python3.12-64

64-bit CPython 3.12

python3.12-64-arm64

64-bit CPython 3.12 on ARM64

/usr/bin/python3

Absolute path, used directly (no search)

>=3.11,<3.13

PEP 440 version specifier (any Python in range)

cpython>=3.11

PEP 440 specifier restricted to CPython

PEP 440 specifiers (>=, <=, ~=, !=, ==, ===) are supported. Multiple specifiers can be comma-separated, for example >=3.11,<3.13.