Getting started¶
Installation¶
pip install python-discovery
Core concepts¶
Before diving into code, here are the key ideas:
Interpreter – a Python executable on your system (e.g.,
/usr/bin/python3.12).Spec – a short string describing what you are looking for (e.g.,
python3.12,pypy3.9,>=3.11).Discovery – the process of searching your system for an interpreter that matches a spec.
Cache – a disk store that remembers previously discovered interpreters so the next lookup is instant.
Inspecting the current interpreter¶
The simplest use case: get information about the Python that is running right now.
flowchart TD
Call["PythonInfo.current_system(cache)"] --> Info["PythonInfo"]
Info --> Exe["executable: /usr/bin/python3.12"]
Info --> Ver["version_info: (3, 12, 1)"]
Info --> Impl["implementation: CPython"]
Info --> Arch["architecture: 64"]
style Call fill:#4a90d9,stroke:#2a5f8f,color:#fff
style Info fill:#4a9f4a,stroke:#2a6f2a,color:#fff
from pathlib import Path
from python_discovery import DiskCache, PythonInfo
cache = DiskCache(root=Path("~/.cache/python-discovery").expanduser())
info = PythonInfo.current_system(cache)
print(info.executable) # /usr/bin/python3.12
print(info.version_info[:3]) # (3, 12, 1)
print(info.implementation) # CPython (or PyPy, GraalPy, etc.)
print(info.architecture) # 64 (or 32)
The returned PythonInfo object contains everything the library knows about that interpreter:
paths, version numbers, sysconfig variables, platform details, and more.
Finding a different interpreter¶
Usually you need a specific Python version, not the one currently running. Pass a spec string
to get_interpreter() to search your system.
flowchart TD
Spec["Spec: python3.12"] --> Call["get_interpreter(spec, cache)"]
Call --> Found{"Match found?"}
Found -->|Yes| Info["PythonInfo with full metadata"]
Found -->|No| Nil["None"]
style Spec fill:#4a90d9,stroke:#2a5f8f,color:#fff
style Info fill:#4a9f4a,stroke:#2a6f2a,color:#fff
style Nil fill:#d94a4a,stroke:#8f2a2a,color:#fff
from pathlib import Path
from python_discovery import DiskCache, get_interpreter
cache = DiskCache(root=Path("~/.cache/python-discovery").expanduser())
result = get_interpreter("python3.12", cache=cache)
if result is not None:
print(result.executable)
You can pass multiple specs as a list – the library tries each one in order and returns the first match.
result = get_interpreter(["python3.12", "python3.11"], cache=cache)
Listing every interpreter¶
When you need every interpreter rather than just the first match – for example, to show the user a chooser, or
to apply your own ranking – use iter_interpreters(). Pass no arguments to enumerate every
implementation python-discovery knows about, or pass a spec to filter.
flowchart TD
Call["iter_interpreters(spec, cache)"] --> Yield["yields PythonInfo"]
Yield --> A["1. try_first_with paths"]
Yield --> B["2. running interpreter"]
Yield --> C["3. PATH (left to right)"]
Yield --> D["4. uv-managed installs"]
style Call fill:#4a90d9,stroke:#2a5f8f,color:#fff
style Yield fill:#4a9f4a,stroke:#2a6f2a,color:#fff
from pathlib import Path
from python_discovery import DiskCache, iter_interpreters
cache = DiskCache(root=Path("~/.cache/python-discovery").expanduser())
for info in iter_interpreters(cache=cache):
print(info.executable, info.version_str, info.implementation)
The result is an iterator, so list(), sorted(), generator expressions and early break all work as you
would expect. Symlinked aliases (/bin/python3 and /usr/bin/python3, or a virtualenv and the base it points at)
collapse to a single entry, so you do not see the same install twice.
To prefer newer interpreters in a range, sort the result by version_info after filtering:
newest_first = sorted(
iter_interpreters(">=3.10,<3.15", cache=cache),
key=lambda info: info.version_info,
reverse=True,
)
Writing specs¶
A spec tells python-discovery what to look for. The simplest form is just a version number like 3.12.
You can add more constraints to narrow the search.
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
Common examples:
Spec |
What it matches |
|---|---|
|
Any Python 3.12 (CPython, PyPy, etc.) |
|
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 hardware |
|
An absolute path, used directly without searching |
|
Any Python in the 3.11–3.12 range (version specifier syntax) |
See the full spec reference for all options.
Parsing a spec¶
You can parse a spec string into its components without searching the system. This is useful for inspecting what a spec means or for building tools on top of python-discovery.
flowchart TD
Input["cpython3.12t-64-arm64"] --> Parse["PythonSpec.from_string_spec()"]
Parse --> Spec["PythonSpec"]
Spec --> impl["implementation: cpython"]
Spec --> ver["major: 3, minor: 12"]
Spec --> ft["free_threaded: True"]
Spec --> arch["architecture: 64"]
Spec --> mach["machine: arm64"]
style Input fill:#4a90d9,stroke:#2a5f8f,color:#fff
style Spec fill:#4a9f4a,stroke:#2a6f2a,color:#fff
from python_discovery import PythonSpec
spec = PythonSpec.from_string_spec("cpython3.12t-64-arm64")
spec.implementation # "cpython"
spec.major # 3
spec.minor # 12
spec.free_threaded # True
spec.architecture # 64
spec.machine # "arm64"
Skipping the cache¶
If you only need to discover once and do not want to write anything to disk, pass cache=None.
Every call will run a subprocess to query the interpreter, so this is slower for repeated lookups.
from python_discovery import get_interpreter
result = get_interpreter("python3.12")
Handling slow interpreter queries¶
On some systems (especially Windows with antivirus or other tools), Python startup is slow. If discovery
times out, increase the timeout using the PY_DISCOVERY_TIMEOUT environment variable.
import os
from python_discovery import get_interpreter
# Allow up to 30 seconds per interpreter
os.environ["PY_DISCOVERY_TIMEOUT"] = "30"
result = get_interpreter("python3.12", cache=cache)
Or, pass it directly in a custom environment dict:
import os
from python_discovery import get_interpreter
env = {**os.environ, "PY_DISCOVERY_TIMEOUT": "30"}
result = get_interpreter("python3.12", env=env, cache=cache)