How to force FindPython to locate a specific version of Python without specifying every last artifact?

Background: the Python build backend py-build-cmake that I maintain needs to invoke CMake to build Python extension modules in user-provided CMake projects. The goal of the build backend is to build these extension modules for a specific version of Python, and if CMake’s FindPython module were to locate the wrong version of Python, this would result in incompatible binaries.

Question: How can I force FindPython to locate this specific version of Python only, and never fall back to e.g. the system-wide Python installation at /usr/bin/python3?

If the user’s CMake project searches for Python using find_package(Python3 COMPONENTS Interpreter Development.Module), it looks like setting Python3_EXECUTABLE does the right thing. However, problems arise when the user only searches for the Development.Module component without the Interpreter: in such a case, it is necessary to set Python3_ROOT_DIR, but this directory may not be unique, it may contain multiple versions of Python, and CMake sometimes picks the wrong one.

For example, if a user installs both Python 3.12 and 3.13 (using make altinstall) in /usr/local, the installation tree could look like this:

/usr/local
├── bin
│   ├── python3.12
│   ├── python3.12-config
│   ├── python3.13
│   └── python3.13-config
├── include
│   ├── python3.12
│   └── python3.13
└── lib
    ├── libpython3.12.so.1.0
    ├── libpython3.13.so.1.0
    ├── libpython3.so
    ├── python3.12
    └── python3.13

When setting Python3_ROOT_DIR=/usr/local, FindPython will always pick Python 3.13, unless I explicitly set all hints, artifacts and result variables such as Python3_INCLUDE_DIR, Python3_SOABI, etc.

Is there a way to guarantee that FindPython ignores any other Python versions? Even if the given Python installation does not satisfy the version range provided in the user’s find_package call, it should not consider any other Python installations that satisfy them, returning NOTFOUND instead.

Related: https://gitlab.kitware.com/cmake/cmake/-/issues/26505

In your case, specifying the required version should be enough:

set(Python3_ROOT_DIR "/usr/local")
find_package(Python3 3.12 EXACT COMPONENTS ...)

or even better:

set(Python3_ROOT "/usr/local")
find_package(Python3 3.12 EXACT COMPONENTS ...)

Thanks! Unfortunately, I don’t have control over the actual find_package call site.

The idea is that the user provides a CMake project that uses find_package somewhere (possibly in multiple places), and then the Python build tools (cibuildwheel, pip, py-build-cmake, scikit-build, etc.) build this user-provided project using a range of Python versions. Control over the Python version would therefore need to happen through the environment or CMake variables.
Is this possible without modifying any user-provided CMake code?

I’ll try to give a more specific example:
A user runs pip install /some/package on a Python package. The package being built has the following code in one of it’s CMakeLists.txt files.

# This code is fixed. It is provided by the user of the build backend.
# As a build backend developer, I have no control over it
# (aside from maybe some guidelines in the documentation).
find_package(Python3 3.10...<3.14 REQUIRED
             COMPONENTS Development.Module)

Now it is the job of the Python build backend (e.g. py-build-cmake) to invoke CMake for this package, in such a way that CMake only finds the very specific version selected by the Python build front-end (e.g. Pip). This usually means that we have the full path of the Python interpreter to use and are able to query it.
The invocation of CMake by the Python build backend then looks like this (simplified):

cmake -B build  -S /some/package \
    -D Python3_SOME_MAGIC_OPTION=/opt/python/bin/python3.12 \
    -D Python3_SOME_ARTIFACT=/opt/python/include/python3.12

Which CMake variables does the build backend need to set to make sure that CMake only considers the intended Python installation?

Finding the given Python installation is one thing, but another important consideration is making sure that CMake doesn’t select some other Python version it found elsewhere on the system, e.g. in case it deems the given /opt/python/bin/python3.12 installation unusable (wrong bitness, not in the CMAKE_FIND_ROOT_PATH when cross-compiling, an incorrect artifact path, a conflicting version requirement in the user’s find_package call, etc.).

For example, let’s say that the user specified a different version range, find_package(Python3 3.13...<3.15 ...), and the Python build tool tries to build the package for Python 3.12. In this case, FindPython should fail rather than returning an arbitrary Python 3.13 or 3.14 installation it found elsewhere.

At minimum, setting the variable Python3_ROOT will ensure the search will be limited to this root directory.
Now, to have a full control of the artifacts selected, without control of the find_package() command is to define, at minima, the following hints:

Python3_EXECUTABLE
Python3_LIBRARY
Python3_INCLUDE_DIR

One possibility, at the top level, is to execute a CMakeLists.txt with the correct find_package() command and reuse the results of this search to set the hints.

Thanks!

Could you point me to the documentation for the Python3_ROOT variable? I didn’t find it on the FindPython page, and the <PackageName>_ROOT variable only seems to be mentioned in the config mode section of the find_package documentation.
Can Python3_ROOT be set to a list of possible root directories?

<PackageName>_ROOT is a CMake feature, this is why there is no specific documentation on it in FindPython documentation.
Indeed, for find_package() command, this is used only in CONFIG mode, but all other find_* commands rely on this variable If called from within a find module. Take a look at these commands for a description of <PackageName>_ROOT variable.