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

Python Enhancement Proposals

PEP 9999 – Wheel Variants: Providers

PEP 9999 – Wheel Variants: Providers

Author:
Jonathan Dekhtiar <jonathan at dekhtiar.com>, Michał Górny <mgorny at quansight.com>, Konstantin Schütze <konstin at mailbox.org>, Ralf Gommers <ralf.gommers at gmail.com>, Andrey Talman <atalman at meta.com>, Charlie Marsh <charlie at astral.sh>, Michael Sarahan <msarahan at gmail.com>, Eli Uriegas <eliuriegas at meta.com>, Barry Warsaw <barry at python.org>, Donald Stufft <donald at stufft.io>, Andy R. Terrel <andy.terrel at gmail.com>
Discussions-To:
Pending
Status:
Draft
Type:
Standards Track
Topic:
Packaging
Requires:
825
Created:
02-Mar-2026
Post-History:
Pending

Table of Contents

Abstract

This PEP extends PEP 825 by defining how variant properties are governed and how their compatibility is determined. This is done either via static feature and feature value compatibility lists specified in the variant metadata, or by querying opt-in plugins that are Python packages, but can also be vendored or reimplemented by the tools. A static list can also be provided in place of querying the plugins. Additionally, an ABI Dependency Variant Provider is defined to facilitate builds of the same package against different dependency versions.

Motivation

PEP 825 defines a protocol for recording additional compatibility data for binary packages, in the form of variant properties, but does not define how to obtain the matching information for the system. This PEP defines how to determine which variant properties are compatible with a particular system and how variant namespaces are governed.

This PEP introduces three different ways of defining and obtaining variant properties for three different use cases:

  1. Variants that express platform compatibility, for example GPU or CPU capabilities. In this case, the goal is to select the best wheel that is compatible with the particular system.
  2. Variants that express non-platform properties, such as different BLAS/LAPACK or OpenMP implementations, or debug builds. Here all variants that were built are compatible, and the goal is to provide users with the ability to explicitly select a non-default variant.
  3. Variants that express compatibility with different dependency versions, particularly aiming to express Application Binary Interface (ABI) compatibility between extension modules. The goal is to enable matching variants against other packages, where one source distribution can be narrowed to specific versions of a dependency during build.

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.

Providers

Variant properties, as defined in PEP 825, are organized into variant namespaces. Every variant namespace used in a variant wheel MUST be governed by a variant provider. Variant providers supply ordered lists of compatible features and feature values corresponding to their namespaces, as required by PEP 825.

The namespace abi_dependency is reserved for the ABI Dependency Variant Provider. The providers for all the other namespaces used MUST be defined in the provider information dictionary in variant metadata. This PEP is concerned only with consuming said dictionary; the process of building variant wheels will be covered in a subsequent PEP.

Each of these providers either defines a static list of compatible features and their values, or specifies a list of requirements referring to Python packages. In the latter case, an API endpoint is either explicitly specified or inferred from the first dependency string. The canonical way of querying the list of compatible features and their values is then to install the specified packages and call this endpoint. The combination of dependency strings and the API endpoint are used to uniquely identify the provider, while the respective version constraints can be used to indicate the minimum versions emitting variant properties used in the package.

For security reasons, none of these packages are installed or used by default. Tools can provide a way to securely use a subset of provider packages, for example through vendoring, a reimplementation or an embedded lockfile. To aid in this, a shared list of such packages along with trusted implementations will be maintained; the details of said list and its governance will be provided in a subsequent PEP. Using provider plugins that are not trusted, such as the actual package from an index fulfilling the requirement, requires explicit user opt-in. This ensures only trusted code is executed during installation, while still supporting the evolution of providers and establishing clear governance over each namespace.

Installers and other tools that need to determine whether a variant is compatible with the system MUST follow the specified algorithm in order to obtain the list of compatible features and feature values for every namespace:

  1. The tool SHOULD implement a way for the user to provide a static list of compatible features and their values for a particular provider. Standardizing this format is left to a future PEP. If the user provided said list, the tool MUST use it exclusively. Otherwise, proceed to step 2.
  2. The tool MUST read the optional key from the provider information to determine whether the variant provider is disabled by default. It SHOULD provide a way to explicitly enable a provider, and MAY provide a way to disable one. If a provider is disabled, the list of compatible features is empty. Otherwise, proceed to step 3.
  3. The tool MUST read the static-properties key from the provider information dictionary. If it is present, the tool MUST use the static lists of properties therein exclusively. Otherwise, proceed to step 4.
  4. If no static list of compatible features and their values is provided, the tool MUST read the requires and plugin-api keys in the provider information dictionary to determine the packages providing the compatibility information. If the tool provides a secure implementation of the specific providers, it SHOULD obtain the list from their implementation. Otherwise, proceed to step 5.
  5. The tool SHOULD provide a way for the user to permit installing specific provider packages. It MUST NOT install or use any provider packages that are not considered trusted. It SHOULD use an isolated virtual environment while installing provider plugin packages.
  6. The tool MUST query the trusted and installed provider packages via the provider plugin API to obtain the list of compatible features and feature values. For providers that were not trusted, it MUST assume that the list of compatible features is empty.

Variant metadata

This PEP extends the metadata introduced in PEP 825 with an additional providers key, described in the Provider information subsection.

(root)
|
+- $schema
+- default-priorities
+- variants
+- providers
   +- {namespace}
      +- optional          : bool       = False
      +- plugin-api        : str | None = None
      +- requires          : list[str]  = []
      +- static-properties
         +- {feature}      : list[str]  = []

This structure corresponds to the version 0.2.0 of the format. An update of the proposed JSON schema for the current format version is included in the Appendix of this PEP. The schema is available in Appendix: JSON Schema for Variant Metadata.

Provider information

Variant metadata in a variant wheel must include a providers key. It corresponds to a dictionary, in which the keys are namespaces, the values are dictionaries with provider information. It specifies how to install and use variant providers. All the namespaces found in default-priorities.namespace MUST be listed in this dictionary, and other keys MUST NOT be present.

The use of provider information is described in the Providers and Provider plugin API sections.

Exactly one of the following keys MUST be present in the provider information dictionary:

  • static-properties: dict[str, list[str]]: The dictionary of static variant compatibility data. The keys correspond to feature names, while the values are the ordered list of compatible feature values. Since the dictionaries in JSON are unsorted, if more than one key is specified, then the order for all features MUST be specified in the feature-order key.
  • requires: list[str]: A list of one or more package dependency specifiers, that are used to install the provider plugin. If the dependency specifiers include environment markers, these are evaluated against the environment where the plugin is being installed, and the requirements for which the markers evaluate to false are filtered out. At least one dependency MUST remain present in every possible environment after filtering. Additionally, if plugin-api is not specified explicitly, the first dependency that remains after filtering MUST always reference the same package name as the first dependency prior to filtering.

A provider information dictionary MAY additionally contain the following key:

  • optional: bool: Whether the provider is optional. Defaults to false. If it is true, the provider is disabled by default and needs to be enabled explicitly.

If the requires key is present, the dictionary MAY additionally contain the following key:

  • plugin-api: str: The API endpoint for the plugin. If it is specified, it MUST be an object reference as explained in the API endpoint section. If it is not specified, a default value is determined by normalizing the package name from the first dependency specifier in requires and then replacing all - characters with _ in the normalized package name.

It is invalid to specify requires or plugin-api if static-properties are present.

If the static-properties key is present, the dictionary MAY additionally contain the following key:

  • feature-order: list[str]: The list controlling the order of compatible features. When present, all keys found in static-properties MUST be listed here. Entries not found in the static-properties dictionary MUST NOT appear in this list.

The providers dictionary is copied into index-level metadata. For the metadata to be consistent, the same keys MUST always correspond to the same values. When combining metadata, the resulting providers dictionary MUST be a union of all the input dictionaries.

Example

Both the variant metadata example and the index-level metadata example from PEP 825 can be extended in the following way:

{
  // The schema URL will be replaced with the final URL on packaging.python.org
  "$schema": "https://variants-schema.wheelnext.dev/peps/9999/v0.2.0.json",

  "default-priorities": {
    // MUST list all namespaces used.
    "namespace": ["x86_64", "aarch64", "blas_lapack"],
  },

  "providers": {
    // MUST list all namespaces used.
    "aarch64": {
      // Specifies provider plugin package. REQUIRED since there is no
      // "static-properties" -- plugin will be queried at install time.
      "requires": ["provider-variant-aarch64 >=0.0.1"]
      // "plugin-api" is OPTIONAL here. It is inferred from "requires":
      // "plugin-api": "provider_variant_aarch64"
    },
    "blas_lapack": {
      // Specifies compatible properties. No install-time querying is
      // necessary.
      "static-properties": {
        // Values are ordered -- by default, openblas variant is preferred
        // over mkl, and accelerate over the other two.
        "library": ["accelerate", "openblas", "mkl"]
      },
    },
    "x86_64": {
      // Specifies provider plugin package. REQUIRED since there is no
      // "static-properties" -- plugin will be queried at install time.
      "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"
    }
  },

  "variants": {
    // same as in PEP 825
  }
}

Provider plugin API

High level design

To make it easier to discover and install plugins, they SHOULD be published in the same indexes as the packages using them. In particular, packages published to PyPI MUST NOT rely on plugins that need to be installed from other indexes.

Except for namespaces reserved as part of this PEP and variant providers using the static-properties key, installable Python packages MUST be provided for plugins. The entire dependency tree of such packages MUST be installable using non-variant wheels, and variant wheels MUST NOT be used while installing them.

As noted in the Providers section, these plugins can also be reimplemented by tools needing them. If that is the case, the API defined in this section does not apply to these implementations. However, the reimplementer is responsible for ensuring consistent behavior with the published provider. The reimplemenetations and vendored providers MUST be matched to the published providers using the requires and plugin-api keys, rather than namespaces.

A plugin implemented as Python package exposes callables that are called via:

{API endpoint}.{callable name}({arguments}...)

These can be implemented either as module-level functions, class methods or static methods. The specifics are provided in the subsequent sections.

API endpoint

The location of the plugin code is called an “API endpoint”, and it is expressed using the object reference notation following the Entry points specification:

{import_path}(:{object_path})?

An API endpoint specification is equivalent to the following Python pseudocode:

import {import_path}

if "{object_path}":
    plugin = {import_path}.{object_path}
else:
    plugin = {import_path}

API endpoints are used in two contexts:

  1. in the plugin-api key of provider information in variant metadata, either explicitly or inferred from the package name in the requires key. This is the primary method of using the plugin when building and installing wheels.
  2. as the value of an installed entry point, as described in variant plugin discovery.

Variant plugin discovery

The Python packages providing variant provider plugins SHOULD install an entry point to facilitate discovery by variant-related utilities. The entry point MUST be placed in the variant_plugins group, its name being the recommended namespace name, and its value being the plugin’s API endpoint. For example, a plugin can declare the following entry point:

[project.entry-points.variant_plugins]
x86_64 = "provider_variant_x86_64"

A tool that facilitates such discovery mechanism MUST collect all entry points in the variant_plugins group and use their respective values to determine the API endpoints to call. They MAY use the entry point name to recommend the namespace to use, and the associated package distribution metadata to determine which package provides a given plugin.

For example, with provider-variant-x86-64==0.0.3 package installing the entry point shown above, variantlib update-pyproject-toml --add x86_64 would add the following metadata to pyproject.toml:

[variant.default-priorities]
namespace = ["x86_64"]

[variant.providers.x86_64]
requires = ["provider-variant-x86-64 >=0.0.3"]

Variant feature config class

The variant feature config class is used as a return value in plugin API functions. It defines a single variant feature, along with a list of possible values. Depending on the context, the order of values MAY be significant. Its instance MUST provide the following properties or attributes:

  • name: str specifying the feature name.
  • multi_value: bool specifying whether the feature is allowed to have multiple corresponding values within a single entry in the variants dictionary. If it is False, then it is an error to specify multiple values for the feature.
  • values: list[str] specifying feature values. In contexts where the order is significant, the values MUST be ordered from the most preferred to the least preferred. This list MUST NOT be empty.

Equivalently, it can be described using the following protocol:

from abc import abstractmethod
from typing import Protocol


class VariantFeatureConfigType(Protocol):
    @property
    @abstractmethod
    def name(self) -> str:
        """Feature name"""
        raise NotImplementedError

    @property
    @abstractmethod
    def multi_value(self) -> bool:
        """Does this property allow multiple values per variant?"""
        raise NotImplementedError

    @property
    @abstractmethod
    def values(self) -> list[str]:
        """List of values, possibly ordered from most preferred to least"""
        raise NotImplementedError

Plugin interface

The plugin interface, as referenced by the API endpoint, MUST provide the following function:

  • get_supported_configs() -> list[VariantFeatureConfigType] that returns a list of feature names and their values that are compatible with the system the plugin is running on. The variant feature and value lists MUST be ordered from the most preferred to the least preferred.

Equivalently, it can be described using the following protocol:

from abc import abstractmethod
from typing import Protocol


class PluginType(Protocol):
    @classmethod
    @abstractmethod
    def get_supported_configs(cls) -> list[VariantFeatureConfigType]:
        """Get ordered lists of compatible features and their values"""
        raise NotImplementedError

Example implementation

from dataclasses import dataclass


@dataclass
class VariantFeatureConfig:
    name: str
    values: list[str]
    multi_value: bool


# internal -- provided for illustrative purpose
_ALL_GPUS = ["narf", "poit", "zort"]


def _get_current_version() -> int:
    """Returns currently installed runtime version"""
    ...  # implementation not provided


def _is_gpu_available(codename: str) -> bool:
    """Is specified GPU installed?"""
    ...  # implementation not provided


def get_supported_configs() -> list[VariantFeatureConfig]:
    current_version = _get_current_version()
    if current_version is None:
        # no runtime found, system not supported at all
        return []

    return [
        VariantFeatureConfig(
            name="min_version",
            # [current, current - 1, ..., 1]
            values=[str(x) for x in range(current_version, 0, -1)],
            multi_value=False,
        ),
        VariantFeatureConfig(
            name="gpu",
            # this may be empty if no GPUs are supported --
            # 'example :: gpu feature' is not supported then;
            # but wheels with no GPU-specific code and only
            # 'example :: min_version' could still be installed
            values=[x for x in _ALL_GPUS if _is_gpu_available(x)],
            multi_value=True,
        ),
    ]

Future extensions

The future versions of this specification, as well as third-party extensions MAY introduce additional attributes on the plugin instances. The implementations SHOULD ignore them.

For best compatibility, all private attributes SHOULD be prefixed with an underscore (_) character to avoid incidental conflicts with future extensions.

ABI Dependency Variant Provider (Optional)

This section describes an OPTIONAL extension to the wheel variant specification. Tools that choose to implement this feature MUST follow this specification. Tools that do not implement this feature MUST treat the variants using it as incompatible, and SHOULD inform users when such wheels are skipped.

The variant namespace abi_dependency is reserved for expressing that different builds of the same version of a package are compatible with different versions or version ranges of a dependency. This namespace MUST NOT be listed in the provider information dictionary nor in the default-priorities.namespace dictionary, and can only appear in the variants dictionary. It is not taken into consideration in variant ordering. Package maintainers SHOULD NOT be publishing variant wheels where two different abi_dependency property sets could be compatible with the same system.

Within this namespace, zero or more properties can be used to express compatible dependency versions. For each property, the feature name MUST be the normalized name of the dependency, whereas the value MUST be a valid release segment of a public version identifier, as defined by the Version specifiers specification. It MUST contain up to three version components, that are matched against the installed version same as the =={value}.* specifier. Notably, trailing zeroes match versions with fewer components (e.g. 2.0 matches release 2 but not 2.1). This also implies that the property values have different semantics than PEP 440 versions, in particular 2, 2.0 and 2.0.0 represent different ranges.

Versions with nonzero epoch are not supported.

Variant Property Matching Rule
abi_dependency :: torch :: 2 torch==2.*
abi_dependency :: torch :: 2.9 torch==2.9.*
abi_dependency :: torch :: 2.8.0 torch==2.8.0.*

Multiple variant properties with the same feature name can be used to indicate wheels compatible with multiple providing package versions, e.g.:

abi_dependency :: torch :: 2.8.0
abi_dependency :: torch :: 2.9.0

This means the wheel is compatible with both PyTorch 2.8.0 and 2.9.0.

Rationale

The primary use case for providers is determining platform compatibility, which implies that they need to be used at install time. The specification proposes a plugin mechanism using Python packages, with the interface inspired by PEP 517. Such a mechanism has a few advantages:

  • The individual plugins can be governed independently, by the stakeholders having necessary knowledge and hardware. Additional compatibility axes (new CPUs, GPUs) do not impose direct maintenance costs on tools interacting with variant wheels, nor on centrally-maintained libraries such as packaging.
  • The plugins can be updated as frequently as necessary, without being tied to tool release schedules.
  • The plugins provide a unified interface for testing new providers. New plugins can be developed and tested locally without having to patch multiple tools, and released to the public after proving the concept.

At the same time, it is understood that installing additional Python packages and running the code from them introduces additional attack vector (as discussed in security implications). For this reason, plugin packages are entirely opt-in, and a few mechanisms are provided to improve the user experience without compromising security:

  • Users can provide static compatibility lists to avoid querying the providers. This also permits deploying packages for different systems than the one running the installer.
  • Tools can maintain their own lists of trusted provider plugins that are enabled by default, or they can vendor or reimplement some providers. This is entirely voluntary, to avoid imposing an additional maintenance burden on tool maintainers. At the same time, the ability to reimplement providers avoids introducing a performance bottleneck on tools that aren’t written in Python.
  • Variant wheels can include static lists of compatible properties, to facilitate variants that do not need querying platform capabilities, such as builds done against different BLAS/LAPACK libraries.

Furthermore, individual providers can be disabled by default (made optional), to introduce variants that can only be selected explicitly, for example debug or experimental builds of packages.

Installing provider plugins in isolated environments is recommended, as that permits tools to automatically deploy them without affecting the system packages. However, this is not a requirement. For example, build frontends may prefer reusing the isolated build environment for this.

Providers are defined as part of the variant metadata. From the metadata consistency requirements, it follows that multiple variant wheels for a single package version published on a single index must always use the same provider for a given namespace. This requirement does not hold across different packages, package versions or indexes. This permits both adjusting dependency bounds and switching to different provider plugins with a new package version.

The requires and plugin-api keys follow the precedent of build-system.requires and build-system.build-backend keys of PEP 517. However, the plugin-api key has been made optional and defaults to being inferred from the package name, reducing boilerplate.

It is recommended for plugins to install entry points to facilitate discovery when installed in development environments. This can be used by variant-related tools, for example to aid debugging variant selection or help configuring source trees.

The ABI Dependency Variant Provider is defined separately, as it needs to interact with the dependency resolver. To avoid adding significant complexity to the plugin API and at the same time restricting the actual implementation, it has been made a special case. It is entirely optional to avoid adding maintenance burden to tool maintainers.

Backwards Compatibility

This PEP does not introduce any new backwards compatibility considerations, compared to PEP 825.

Security Implications

This PEP introduces a plugin system for querying the platform capabilities. Tools may install these packages and execute the code within them during dependency resolution or wheel processing. This elevates the supply-chain attack potential by introducing two new points for malicious actors to inject arbitrary code payload:

  1. Publishing a version of a variant provider plugin or one of its dependencies with malicious code.
  2. Introducing a malicious variant provider plugin in an existing package metadata.

While such attacks can already be done to the package’s dependencies, it is prudent not to add another attack surface.

A similar issue already exists in the packaging ecosystem when packages are installed from source distributions, with build backends and other build dependencies are being installed and executed by default. Users are often unaware of this risk. Securing build dependencies is non-trivial and not commonly done. More commonly, users opt-out of source distribution builds altogether instead.

Unfortunately, an opt-in system creates a risk of security fatigue. Users wishing to use variant wheels may start blanket-enabling all use of provider plugins, reintroducing the RCE danger. To avoid this and improve user experience, the PEP proposes that tool provide a selected set of trusted plugins, only requiring opt-in for uncommon plugins. A subsequent PEP will give further recommendations for improved user experience.

How to Teach This

This PEP is focused on installing variant wheels. The primary source of information for the users will be the user interface of installers, supplemented by their documentation and installation instructions of specific packages publishing variant wheels. The documentation present on packaging.python.org will need to be updated as well.

Ideally, in the most common use cases variants will work out of the box and non-expert users will not need to be aware of them, much like they do not need to be aware of Platform compatibility tags. Some packages will be able to simplify their installation instructions to a single command covering the different hardware accelerators.

Expert users will need guidance that will largely be specific to particular installer implementation, as it involves user interface decisions. The following topics may need to be covered, depending on the features implemented by the installer:

  • how to enable installing untrusted variant provider packages, and what are the security implications of that
  • how to enable optional providers
  • how to generate and provide static compatibility data, enabling deployment for remote targets
  • how to explicitly select a specific variant
  • how to alter variant selection, for example by specifying preferred properties or filtering out undesirable properties

The topic of teaching package maintainers will be addressed in a subsequent PEP, along with building variant wheels.

Reference Implementation

The variantlib project contains a reference implementation of this PEP.

Rejected Ideas

An approach without provider plugins

Rather than introducing provider plugins, the rules governing every variant namespace could be defined via PEPs. However, such an approach would be less scalable and impose additional effort on stakeholders, PEP editors and tool maintainers.

Every new namespace would have to go through standardization process, followed by explicit implementation process. Deployment of new variant properties would be entirely dependent on tool updates. The added maintenance cost could lead to support for less popular variant axes not being accepted, or lack of feature parity between different tools.

Acknowledgements

This work would not have been possible without the contributions and feedback of many people in the Python packaging community. In particular, we would like to credit the following individuals for their help in shaping this PEP (in alphabetical order):

Alban Desmaison, Bradley Dice, Chris Gottbrath, Dmitry Rogozhkin, Emma Smith, Geoffrey Thomas, Henry Schreiner, Jeff Daily, Jeremy Tanner, Jithun Nair, Keith Kraus, Leo Fang, Mike McCarty, Nikita Shulga, Paul Ganssle, Philip Hyunsu Cho, Robert Maynard, Vyas Ramasubramani, and Zanie Blue.

Change History

  • xx-yyy-2026
    • Initial version, split from PEP 817 draft.
    • Namespaces have been removed from the provider plugin API. Instead, the namespace is named by the package in variant metadata.
    • The enable-if and install-time keys have been removed from provider information, as they were deemed redundant.
    • The static-properties table has been moved into provider information, and made mutually exclusive with the requires key.
    • The feature-order key has been added to facilitate the static property ordering after the removal of default-priorities.feature from PEP 825.
    • The values member of the variant feature config class is now required to be non-empty.
    • The entry point name is now used to provide the recommended namespace for a provider.

Appendices