Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 9999 – Wheel Variants: Building

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

Table of Contents

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 variants dictionary
  • the variant label matches that key
  • there are entries in the default-priorities.namespace list and the providers dictionary for all namespaces found in variant properties
  • when providers.{namespace}.static-properties specifies more than one feature name, providers.{namespace}.feature-order specifies ordering for all listed features

Additionally, tools MAY verify that:

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.toml file, as described in pyproject.toml integration.
  • The available variants are described in the variant.variants table in said file, except for the null variant which is always valid.
  • The config_settings dictionary of hooks used to build wheels accepts a variant-label key that enables building a variant wheel with the specified label. The corresponding variant properties are obtained from the respective entry in the variant.variants table in pyproject.toml, except for the null label which corresponds to an empty property list.
  • The get_requires_for_build_wheel() hook of PEP 517, the get_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 $schema key specifies the JSON schema of the variant metadata output
  • the default-priorities dictionary is used to seed default-priorities in the wheels
  • the providers dictionary is used to seed providers in the wheels and to query providers at build time, if necessary
  • the variants dictionary provides a mapping of valid variant labels to variant properties, to facilitate requesting a build of a specific variant (null variant 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:

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 optional plugin-api
  • build-requires, with optional plugin-api (only in pyproject.toml)
  • static-properties, with optional feature-order
  • builtin

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 to True, then all valid feature names and values are always compatible. In that case, the get_supported_configs() function MUST always return the same value as get_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-requires key is used in place of requires key
  • all providers are assumed to be enabled and trusted

When populating static properties, the tool MUST:

  1. Ensure that the loaded plugin has all_properties_compatible attribute set to True. It is invalid to specify a plugin that doesn’t satisfy that criterion via build-requires.
  2. Call get_supported_configs() to obtain the ordered lists of supported feature names and their values.
  3. 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 by static-properties.
  4. Remove the build-requires and plugin-api keys from the provider metadata.
  5. Add static-properties key to the provider metadata, populating it using the lists obtained from get_supported_configs().
  6. If get_supported_configs() returned more than one feature name, set providers.{namespace}.feature-order to 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 variants dictionary is now permitted in pyproject.toml metadata, and it can be used to define available variants to build.
    • A new build-requires key was introduced in provider metadata. It can only be used in pyproject.toml and it is used to populate static-properties from a provider plugin at build time. It replaced the previous combination of install-time = false with non-empty requires.
    • The is_aot_plugin attribute of the plugin interface has been renamed to all_properties_compatible.