Skip to content
Back to top

# 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)

## Customize interpreter query timeout

On slower systems (especially Windows), Python startup can take more than the default 15 seconds. If your discovery process times out when looking for interpreters, you can extend the timeout via the `PY_DISCOVERY_TIMEOUT` environment variable.

    import os

    from python_discovery import get_interpreter

    # Increase timeout to 30 seconds for slow environments
    env = {**os.environ, "PY_DISCOVERY_TIMEOUT": "30"}
    result = get_interpreter("python3.12", env=env)

The timeout value should be a number in seconds. Each interpreter candidate is given this much time to respond. If a timeout occurs, the candidate is skipped and the search continues with the next one.

## List every interpreter on the system

Use `iter_interpreters()` to enumerate every Python python-discovery can find. With no spec it yields all known implementations (CPython, PyPy, GraalPy – see `KNOWN_IMPLEMENTATIONS`). Pass a spec to filter, exactly like `get_interpreter()`.

    from pathlib import Path

    from python_discovery import DiskCache, iter_interpreters

    cache = DiskCache(root=Path("~/.cache/python-discovery").expanduser())

    # Every interpreter, no filter
    for info in iter_interpreters(cache=cache):
        print(info.executable, info.version_str, info.implementation)

    # Every CPython 3.10 or newer, newest first
    newest_first = sorted(
        iter_interpreters("cpython>=3.10", cache=cache),
        key=lambda info: info.version_info,
        reverse=True,
    )

Enumeration interrogates every candidate as a subprocess on a cold cache. Always pass a `DiskCache` if you call this more than once.

## Pick an interpreter from a range, preferring newer

A common need: search a version range and prefer newer interpreters when more than one matches. Sort the result of `iter_interpreters()` and take the first.

    from pathlib import Path

    from python_discovery import DiskCache, iter_interpreters

    cache = DiskCache(root=Path("~/.cache/python-discovery").expanduser())

    matches = sorted(
        iter_interpreters("cpython>=3.10,<3.15", cache=cache),
        key=lambda info: info.version_info,
        reverse=True,
    )
    info = matches[0] if matches else None

Use `get_interpreter()` instead when you only need the first PATH-priority hit; use the sort-and-take pattern when your ordering differs from PATH order (newest version, smallest install size, preferred install root, etc.).

## 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.