PEP 9999 – Wheel Variants: Building
- Author:
- Michał Górny <mgorny at quansight.com>, Jonathan Dekhtiar <jonathan at dekhtiar.com>, Konstantin Schütze <konstin at mailbox.org>, Ralf Gommers <ralf.gommers at gmail.com>
- Discussions-To:
- Pending
- Status:
- Draft
- Type:
- Standards Track
- Topic:
- Packaging
- Requires:
- 825, 850
- Created:
- 02-Mar-2026
- Post-History:
- Pending
Abstract
This PEP extends PEP 825 and PEP 850 by specifying how variant
wheels are built. The recommended approach is to store a variant
metadata seed, along with the definition of supported variants, in
pyproject.toml. Build backends accept a variant-label setting
key that enables building a specific variant wheel. The plugin API is
extended to support validating variant properties in built wheels, and
to support seeding static-properties from plugins invoked at build
time.
Motivation
PEP 825 and PEP 850 introduced variant wheels while focusing on the file format and installation process. While technically this is sufficient for tools to build working variant wheels, it would likely lead to proliferation of different interfaces and inconsistencies in user experience.
This PEP considers a variety of possible tools that may emit variant
wheels, including build backends, build frontends and tools dedicated to
converting regular wheels into variant wheels, and proposes standardized
format for storing variant metadata and possible variants in the project
tree using pyproject.toml and interface for interacting with build
backends. All of this is given as a recommendation to make documentation
universal, build backends interchangeable and therefore improve user
experience. At the same, build backends are permitted to follow a
different implementation path as necessary to fit their specific use
cases.
Specification
Definitions
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
Building variant wheels
Tools MAY support building variant wheels. When building variant wheels, they MUST ensure that the produced wheels conform to PEP 825 and PEP 850. In particular, tools MUST ensure that:
- the variant metadata conforms to the schema
- there is only a single key in the
variantsdictionary - the variant label matches that key
- there are entries in the
default-priorities.namespacelist and theprovidersdictionary for all namespaces found in variant properties - when
providers.{namespace}.static-propertiesspecifies more than one feature name,providers.{namespace}.feature-orderspecifies ordering for all listed features
Additionally, tools MAY verify that:
- all specified variant properties are valid according to the respective providers (using extensions to provider plugin API)
Build backend support
Build backends MAY support building variant wheels. However, PEP 517 and PEP 660 hooks MUST NOT build variant wheels, unless they are explicitly requested by the user.
For consistency, it is RECOMMENDED that build backends implement the following interface for building variant wheels:
- Variant metadata seed is stored in the
pyproject.tomlfile, as described in pyproject.toml integration. - The available variants are described in the
variant.variantstable in said file, except for the null variant which is always valid. - The
config_settingsdictionary of hooks used to build wheels accepts avariant-labelkey that enables building a variant wheel with the specified label. The corresponding variant properties are obtained from the respective entry in thevariant.variantstable inpyproject.toml, except for thenulllabel which corresponds to an empty property list. - The
get_requires_for_build_wheel()hook of PEP 517, theget_requires_for_build_editable()hook of PEP 660, and similar hooks include the provider plugin packages for the variant being built, and the build backend uses the package installed in the build environment as a result.
pyproject.toml integration
This specification extends the pyproject.toml file originally
defined in PEP 518. Said file MAY contain a top-level variant
table that contains variant metadata for the project. The contents of
this table are defined by PEP 850 or a subsequent specification, with
the extensions listed in this PEP, converted into the respective TOML
types. When building variant wheels, tools SHOULD use the information
from this file to seed the variant metadata in the built variant wheels.
More specifically:
- the
$schemakey specifies the JSON schema of the variant metadata output - the
default-prioritiesdictionary is used to seeddefault-prioritiesin the wheels - the
providersdictionary is used to seedprovidersin the wheels and to query providers at build time, if necessary - the
variantsdictionary provides a mapping of valid variant labels to variant properties, to facilitate requesting a build of a specific variant (nullvariant does not need to be specified)
Tools MAY provide additional methods of providing the necessary variant metadata.
Extension to provider metadata
The provider information dictionaries used as values in the
providers dictionary MAY contain the following key:
build-requires: list[str]: A list of one or more dependency specifiers, same asrequiresbut used to populatestatic-propertiesat build time, as explained in the populating static properties from provider plugins section.
The plugin-api key can be used with build-requires in the same
way that it is used with requires.
If the build-requires key is specified, then the builtin,
feature-order, requires and static-properties keys MUST NOT
be present.
The build-requires key is only valid in the pyproject.toml file.
In a variant wheel built from that file, the build-requires and
plugin-api keys MUST be removed, and static-properties MUST be
added instead. If the resulting static-properties dictionary lists
more than one feature, then the feature-order key MUST be added as
well.
Consequently, in addition to the optional key, the following key
combinations are valid:
requires, with optionalplugin-apibuild-requires, with optionalplugin-api(only inpyproject.toml)static-properties, with optionalfeature-orderbuiltin
Further rules governing when the optional keys are required are specified in PEP 850.
Example
The pyproject.toml file corresponding to the package with variant
metadata equivalent to the one provided in the example in PEP 850
could look like the following:
[variant]
"$schema" = "https://variants-schema.wheelnext.dev/peps/850/v0.2.0.json"
[variant.default-priorities]
# REQUIRED: specifies that x86_64 CPU properties are more important than
# aarch64 CPU properties (both are mutually exclusive, so the exact order
# does not matter), and both are more important than specific BLAS/LAPACK
# library: The "abi" namespace using a builtin provider is listed last,
# it does not influence ordering here.
namespace = ["x86_64", "aarch64", "blas_lapack", "abi"]
# REQUIRED: variant providers
# ---------------------------
# (keys must match default-priorities.namespace)
[variant.providers.aarch64]
# Specifies provider plugin package; the plugin will be queried
# at install time. REQUIRED, mutually exclusive with the other required
# keys.
requires = [
"provider-variant-aarch64 >=0.0.1",
]
# "plugin-api" is OPTIONAL here. It is inferred from "requires".
# plugin-api = "provider_variant_aarch64"
[variant.providers.abi]
# Specifies that this a builtin provider. REQUIRED, mutually exclusive
# with the other required keys.
builtin = "abi_dependency"
[variant.providers.blas_lapack]
# Specifies that "static-properties" are to be populated from a provider
# plugin package; it will be queried at build time. REQUIRED, mutually
# exclusive with the other required keys.
build-requires = ["blas-lapack-variant-provider"]
[variant.providers.x86_64]
# Specifies provider plugin package; the plugin will be queried
# at install time. REQUIRED, mutually exclusive with the other required
# keys.
requires = [
"provider-variant-x86-64 >=0.0.1; python_version >= '3.12'",
"legacy-provider-variant-x86-64 >=0.0.1; python_version < '3.12'",
]
# Overrides plugin API endpoint. REQUIRED since "requires" may
# evaluate to two different packages depending on Python version.
plugin-api = "provider_variant_x86_64.plugin:X8664Plugin"
# RECOMMENDED: variants that can be built
# ---------------------------------------
# "x86_64_v3_openblas" label corresponds to:
# - blas_lapack :: library :: openblas
# - x86_64 :: level :: v3
[variant.variants.x86_64_v3_openblas]
blas_lapack.library = ["openblas"]
x86_64.level = ["v3"]
# "x86_64_v4_mkl" label corresponds to:
# - blas_lapack :: library :: mkl
# - x86_64 :: level :: v4
[variant.variants.x86_64_v4_mkl]
blas_lapack.library = ["mkl"]
x86_64.level = ["v4"]
Extensions to provider plugin API
Provider plugins MAY be used at build time, if the provider metadata
specifies either requires or build-requires key, per the
extension to provider metadata section.
The plugin interface specified in PEP 850 is extended by adding the following REQUIRED function:
get_all_configs() -> list[VariantFeatureConfigType]that returns a list of variant feature names and their values that are valid according to the provider plugin. The ordering of the lists is insignificant. A particular plugin version MUST always return the same lists (modulo ordering), irrespective of any runtime conditions.
The value returned by get_supported_configs() MUST be a subset of
the feature names and values returned by get_all_configs() (modulo
ordering).
Additionally, the following OPTIONAL attribute is added:
all_properties_compatible: bool = False; if it is set toTrue, then all valid feature names and values are always compatible. In that case, theget_supported_configs()function MUST always return the same value asget_all_configs()(modulo ordering).
The resulting interface can be described using the following protocol:
from abc import abstractmethod
from typing import Protocol
class PluginType(Protocol):
# Note: properties are used here for docstring purposes, these must
# be actually implemented as attributes.
@property
def all_properties_compatible(self) -> bool:
"""
Is this plugin valid for use with `build-requires`?
If this is True, then `get_supported_configs()` must always
return the same values, irrespective of the platform used.
This permits the plugin to be used via `build-requires`,
where the supported properties are recorded at build time.
If the value of `get_supported_configs()` may change in any way
depending on the platform used, then it must be False
(the default).
"""
return False
@classmethod
@abstractmethod
def get_all_configs(cls) -> list[VariantFeatureConfigType]:
"""Get all valid configs for the plugin"""
raise NotImplementedError
@classmethod
@abstractmethod
def get_supported_configs(cls) -> list[VariantFeatureConfigType]:
"""Get ordered lists of compatible features and their values"""
raise NotImplementedError
Example implementation
The example plugin implementation from PEP 850 can be extended in the following way:
from dataclasses import dataclass
@dataclass
class VariantFeatureConfig:
name: str
values: list[str]
multi_value: bool
# internal -- provided for illustrative purpose
_MAX_VERSION = 4
_ALL_GPUS = ["narf", "poit", "zort"]
def get_supported_configs() -> list[VariantFeatureConfig]:
... # same as in PEP 850
def get_all_configs() -> list[VariantFeatureConfig]:
return [
VariantFeatureConfig(
name="min_version",
# [1, 2, 3, 4] (the order does not matter)
values=[str(x) for x in range(1, _MAX_VERSION + 1)],
# single-valued, since there is always one minimum
multi_value=False,
),
VariantFeatureConfig(
name="gpu",
# [narf, poit, zort]
values=_ALL_GPUS,
# multi-valued, since a package can target multiple GPUs
multi_value=True,
),
]
Example implementation of a plugin suitable for build-time use
from dataclasses import dataclass
@dataclass
class _VariantFeatureConfig:
name: str
values: list[str]
multi_value: bool
all_properties_compatible = True
def get_supported_configs() -> list[_VariantFeatureConfig]:
return [
VariantFeatureConfig(
name="library",
values=["accelerate", "openblas", "mkl"],
multi_value=False,
),
]
def get_all_configs() -> list[_VariantFeatureConfig]:
return get_supported_configs()
Populating static properties from provider plugins
If the variant provider metadata contains the build-requires key (as
defined in the extension to provider metadata section), the specified
provider plugin is used to populate the static-properties dictionary
while building the variant wheel.
The packages to install and the plugin interface to use are determined as specified in PEP 850, except that:
- the
build-requireskey is used in place ofrequireskey - all providers are assumed to be enabled and trusted
When populating static properties, the tool MUST:
- Ensure that the loaded plugin has
all_properties_compatibleattribute set toTrue. It is invalid to specify a plugin that doesn’t satisfy that criterion viabuild-requires. - Call
get_supported_configs()to obtain the ordered lists of supported feature names and their values. - Verify that all properties the wheel is built with are compatible per
get_supported_configs(). If that weren’t the case, the resulting wheel would include properties that are not permitted bystatic-properties. - Remove the
build-requiresandplugin-apikeys from the provider metadata. - Add
static-propertieskey to the provider metadata, populating it using the lists obtained fromget_supported_configs(). - If
get_supported_configs()returned more than one feature name, setproviders.{namespace}.feature-orderto the ordered list of features.
Note that static-properties MUST be populated even if no properties
from the provider’s namespace are used, as otherwise metadata across
different variant wheels would be inconsistent.
Rationale
The standard pyproject.toml file is used to avoid spreading the
project configuration across multiple files. The format used matches the
one used for variant metadata, except for the additions described in
this specification.
The variant.$schema key refers to the JSON schema of the metadata
produced by the tool. Importantly, this schema does not cover the TOML
extensions given in this PEP, as they are not present in the resulting
JSON. No separate schema is provided for the TOML table.
When using build backends, using get_requires*() hooks for exposing
provider plugin dependencies as regular build dependencies is
recommended. This permits reusing the existing frontend logic for
creating isolated environments and installing packages.
The provider plugin API is extended to enable verifying whether the
properties specified for a variant are correct. To this purpose, a
get_all_configs() function is introduced that is symmetric to
get_supported_configs() introduced in PEP 850. However, while the
latter needs to return features and their values in a meaningful order,
the former is merely used for verification and permits any order for
provider author’s convenience. The build tools are not required to
perform this validation, as it requires installing and querying provider
plugins.
Furthermore, this PEP permits populating static-properties using
provider plugins. This needs both to be supported by the plugin in
question, and explicitly enabled in the package, through using
build-requires in place of requires. This facilitates
synchronizing the feature names and values across multiple packages,
for example BLAS/LAPACK library names. The process is handled entirely
at build time, and the resulting wheel is fully compliant with
PEP 850.
Technically, the presence of both get_supported_configs() and
get_all_configs() when all properties are compatible is redundant:
both must return the same values However, the specification requires
both to keep a single interface for all provider plugins.
Backwards Compatibility
As noted in PEP 825, variant wheels are incompatible with the existing tooling. To avoid disrupting existing workflows, this PEP requires (in build backend support) that build backends do not build variant wheels by default.
The introduction of a new variant table (in pyproject.toml
integration) follows from PEP 518 specifying that such tables are
“reserved for future use by other PEPs”. Therefore, introducing the
[variant] table is backwards compatible.
Security Implications
There are no new security implications beyond what is already noted in
PEP 850. In particular, the use of provider plugins during build time
is not seen as an additional risk, given that the dependencies are
provided by the user directly (for example, in pyproject.toml) and
they are treated equivalently to other build dependencies.
How to Teach This
This PEP is primarily oriented towards package maintainers. The primary
source of information will be the documentation of build backends and
other tools that support building variant wheels. The documentation
present on packaging.python.org will also be updated for the
recommended build backend interface.
Reference Implementation
The variantlib project provides a reference implementation of low-level library functions for this PEP. The pep_817_wheel_variants monorepo includes demonstration ports of a few build systems.
Change History
- xx-yyy-2026
- Initial version, split from PEP 817 draft.
- A specific build backend interface for building variant wheels was proposed.
- The
variantsdictionary is now permitted inpyproject.tomlmetadata, and it can be used to define available variants to build. - A new
build-requireskey was introduced in provider metadata. It can only be used inpyproject.tomland it is used to populatestatic-propertiesfrom a provider plugin at build time. It replaced the previous combination ofinstall-time = falsewith non-emptyrequires. - The
is_aot_pluginattribute of the plugin interface has been renamed toall_properties_compatible.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.