How-to guides

Search specific directories first

If you know a likely location for the interpreter, pass it via try_first_with to check there before the normal search. This is useful when you have a custom Python install outside the standard locations.

from python_discovery import get_interpreter

info = get_interpreter("python3.12", try_first_with=["/opt/python/bin"])
if info is not None:
    print(info.executable)

Restrict the search environment

By default, python-discovery reads environment variables like PATH and PYENV_ROOT from your shell. You can override these to control exactly where the library looks.

        flowchart TD
    Env["Custom env dict"] --> Call["get_interpreter(spec, env=env)"]
    Call --> PATH["PATH"]
    Call --> Pyenv["PYENV_ROOT"]
    Call --> UV["UV_PYTHON_INSTALL_DIR"]
    Call --> Mise["MISE_DATA_DIR"]

    style Env fill:#4a90d9,stroke:#2a5f8f,color:#fff
    
import os

from python_discovery import get_interpreter

env = {**os.environ, "PATH": "/usr/local/bin:/usr/bin"}
result = get_interpreter("python3.12", env=env)

Read interpreter metadata

Once you have a PythonInfo, you can inspect everything about the interpreter.

        classDiagram
    class PythonInfo {
        +executable: str
        +system_executable: str
        +implementation: str
        +version_info: VersionInfo
        +architecture: int
        +platform: str
        +sysconfig_vars: dict
        +sysconfig_paths: dict
        +machine: str
        +free_threaded: bool
    }
    
from pathlib import Path

from python_discovery import DiskCache, get_interpreter

cache = DiskCache(root=Path("~/.cache/python-discovery").expanduser())
info = get_interpreter("python3.12", cache=cache)

info.executable           # Resolved path to the binary.
info.system_executable    # The underlying system interpreter (outside any venv).
info.implementation       # "CPython", "PyPy", "GraalPy", etc.
info.version_info         # VersionInfo(major, minor, micro, releaselevel, serial).
info.architecture         # 64 or 32.
info.platform             # sys.platform value ("linux", "darwin", "win32").
info.machine              # ISA: "arm64", "x86_64", etc.
info.free_threaded        # True if this is a no-GIL build.
info.sysconfig_vars       # All sysconfig.get_config_vars() values.
info.sysconfig_paths      # All sysconfig.get_paths() values.

Implement a custom cache backend

The built-in DiskCache stores results as JSON files with filelock-based locking. If you need a different storage strategy (e.g., in-memory, database-backed), implement the PyInfoCache protocol.

        classDiagram
    class PyInfoCache {
        <<Protocol>>
        +py_info(path) ContentStore
        +py_info_clear() None
    }
    class ContentStore {
        <<Protocol>>
        +exists() bool
        +read() dict | None
        +write(content) None
        +remove() None
        +locked() context
    }
    class DiskCache {
        +root: Path
    }
    PyInfoCache <|.. DiskCache
    PyInfoCache --> ContentStore
    
from pathlib import Path

from python_discovery import ContentStore, PyInfoCache


class MyContentStore:
    def __init__(self, path: Path) -> None:
        self._path = path

    def exists(self) -> bool: ...

    def read(self) -> dict | None: ...

    def write(self, content: dict) -> None: ...

    def remove(self) -> None: ...

    def locked(self): ...


class MyCache:
    def py_info(self, path: Path) -> MyContentStore: ...

    def py_info_clear(self) -> None: ...

Any object that matches the protocol works – no inheritance required.