Python’s existing wheel packaging format uses Platform compatibility tags to specify a given wheel’s supported environments. These tags are unable to express modern hardware configurations and their features, such as the availability of GPU acceleration. The tags fail to provide custom package variants, such as builds against different dependency ABIs. These inabilities are particularly challenging for scientific computing, artificial intelligence (AI), machine learning (ML), and high-performance computing (HPC) communities.
This PEP proposes “Wheel Variants”, an extension to the Binary distribution format. This extension introduces a mechanism for package maintainers to declare multiple build variants for the same package version, while allowing installers to automatically select the most appropriate variant based on system hardware and software characteristics. More specifically, it proposes:
The goal is for the obvious installation commands ({tool} install <package>) to select the most appropriate wheel, and provide the best user experience.
The 2024 Python Developers Survey shows that a significant proportion of Python’s users have scientific computing use-cases. This includes data analysis (40% of respondents), machine learning (30%), and data engineering (30%). Many of the software packages developed for these areas rely on diverse hardware features that cannot be adequately expressed in the current wheel format, as highlighted in the limitations of platform compatibility tags.
For example, packages such as PyTorch need to be built for specific CUDA or ROCm versions, and that information cannot currently be included in the wheel tag. Having to build multiple wheels targeting very different hardware configurations forces maintainers into various distribution strategies that are suboptimal, and create friction for users and authors of other software who wish to depend on the package in question.
A few existing approaches are explored in Current workarounds and their drawbacks. They include maintaining separate package indexes for different hardware configurations, bundling all potential variants into a single wheel of considerable size, or using separate package names (mypackage-gpu, mypackage-cpu, etc.). Each of these approaches has significant drawbacks and potential security implications.
The current wheel format encodes compatibility through three platform tags:
These tags are limited to expressing the most fundamental properties of the Python interpreter, operating system and the broad CPU architectures. They cannot express anything more detailed, including non-CPU hardware requirements or library ABI constraints.
This lack of flexibility has led many projects to find sub-optimal - yet necessary - workarounds, such as the manual installation command selector provided by the PyTorch team. This complexity represents a fundamental scalability issue with the current tag system that is not extensible enough to handle the combinatorial complexity of build options.
Projects such as NumPy currently resort to building wheels for a baseline CPU target, and using runtime dispatching for performance-critical routines. Such a solution requires additional effort from package maintainers, and usually doesn’t let the code benefit from compiler optimizations outside the few select functions.
For comparison, building GROMACS for higher CPU baselines proved to provide significant speedups:
Performance of GROMACS 2020.1 built for different generations of CPUs. Vertical axis shows performance expressed in ns/day, a GROMACS-specific measure of simulation speed (higher is better).
Compiling GROMACS for architectures that can exploit the AVX-512 instructions supported by the Intel Cascade Lake microarchitecture gives an additional 18% performance improvement relative to using AVX2 instructions, with a speedup of about 70% compared to a generic GROMACS installation with only SSE2.
—archspec: A library for detecting, labeling, and reasoning about microarchitectures
Projects such as PyTorch and RAPIDS currently distribute packages that approximate “variants” through separate package indexes with custom URLs. We will use the example of PyTorch, while the problem, the workarounds, and the impact on users also apply to other packages.
The PyTorch install selector (https://pytorch.org/get-started/locally/, captured 22-Aug-2025)
PyTorch uses a combination of index URLs per accelerator type and local version segments as accelerator tag (such as +cu130, +rocm6.4 or +cpu) . Users need to first determine the correct index URL for their system, and add an index specifically for PyTorch.
Tools need to implement special handling for the way PyTorch uses local version segments. These requirements break the pattern that packages are usually installed with. Problems with installing PyTorch are a very common point of user confusion. To quantify this, on 2025-12-05, 552 out of 8136 (6.8%), of issues on uv’s issue tracker contained the term “torch”.
Security Risk: This approach has unfortunately led to supply chain attacks - more details on the PyTorch Blog. It’s a non-trivial problem to address which has forced the PyTorch team to create a complete mirror of all their dependencies, and is one of the core motivations behind PEP 766.
The complexity of configuration often leads to projects providing ad-hoc installation instructions that do not provide for seamless package upgrades.
Packages such as XGBoost use different package names to approximate variants:
Maintainers of other software cannot express that they depend on either of the available variants being selected. They need to either depend on a specific variant, provide multiple alternative dependency sets using extras, or even publish their own software using multiple package names matching upstream variants.
Commonly, these packages install overlapping files. Since Python packaging does not support expressing that two packages are mutually exclusive, installers can install both of them to the same environment, with the package installed second overwriting files from the one installed first. This leads to runtime errors, and the possibility of incidentally switching between variants depending on the way package upgrades are ordered.
An additional limitation of this approach is that publishing a new release synchronously across multiple package names is not currently possible. PEP 694 proposes adding such a mechanism for multiple wheels within a single package, but extending it to multiple packages is not a goal.
Security Risk: proliferation of suffixed variant packages leads users to expect these suffixes in other packages, making name squatting much easier. For example, one could create a malicious numpy-cuda package that users will be lead to believe it’s a CUDA variant of NumPy.
As of the time of writing, CuPy has already registered a total of 55 cupy* packages with different names, most of them never actually used (they are only visible through the use of Simple API), and a large part of the remaining ones no longer updated. This clearly highlights the magnitude of the problem, and the effort put into countering the risk of name squatting.
JAX uses a plugin-based approach. The central jax package provides a number of extras that can be used to install additional plugins, e.g. jax[cuda12] or jax[tpu]. This is far from ideal as pip install jax (with no extra) leads to a nonfunctional installation, and consequently dependency chains, a fundamental expected behavior in the Python ecosystem, are dysfunctional.
JAX includes 12 extras to cover all use cases - many of which overlap and could be misleading to users if they don’t read the documentation in detail. Most of them are technically mutually exclusive, though it is currently impossible to correctly express this within the package metadata.
Including all possible variants in a single wheel is another option, but this leads to excessively large artifacts, wasting bandwidth and leading to slower installation times for users who only need one specific variant. In some cases, such artifacts cannot be hosted on PyPI because they exceed its size limits.
FlashAttention does not publish wheels on PyPI at all, but instead publishes a customized source distribution that performs platform detection, downloads the appropriate wheel from an upstream server, and then provides it to the installer. This approach can select the optimal variant automatically, but it prevents binary-only installs from working, requires a slow and error-prone build via a source distribution, and breaks common caching assumptions tied to the wheel filename. It also requires a specially prepared build environment that contains the torch package matching the version that the software will run against, which requires building without build isolation. On the project side, it requires hosting wheels separately.
Security Risk: Similar to regular source builds, this model requires running arbitrary code at install time. The wheels are downloaded entirely outside the package manager’s control, extending the attack surface to two separate wheel download implementations and preventing proper provenance tracking.
The lack of standardized support for solving against hardware and ABI requirements has led to ecosystem fragmentation:
The packaging limitations particularly affect scientific computing and AI/ML applications where performance optimization is critical:
Wheel variants are a clear step in the right direction in this regard.
—Michael Hudgins, JAX Developer Infrastructure Lead
They affect everyone from package authors to end users of all skill levels, including students, scientists and engineers:
—Leah Wasser, Executive Director and Founder of pyOpenSci
Research institutions and cloud providers manage heterogeneous computing clusters with different architectures (CPU, Hardware accelerators, ASICS, etc.). The current system requires environment-specific installation procedures, making reproducible deployment difficult. This situation also contributes to making “scientific papers” difficult to reproduce. Application authors focused on improving that are hindered by the packaging hurdles too:
—Carlos Córdoba, lead developer of the Spyder IDE
The recent advances in modern AI workflows increasingly rely on GPU acceleration, but the current packaging system makes deployment complex and adds a significant burden on open source developers of the entire tool stack (from build backends to installers, not forgetting the package maintainers).
With the number of hardware we support growing rapidly again, we are very supportive of the wheel variants efforts that will allow us to get PyTorch install instructions to be what our users have been expecting since PyTorch was first released: pip install torch
—The PyTorch Core Maintainers
The lead maintainer of XGBoost enumerates a number of problems XGBoost has that he expects will be addressed by wheel variants:
—Philip Hyunsu Cho, a lead maintainer of XGBoost
The complexity of packaging is distracting developers from focusing on the actual goals for their software:
In recognition of this, my team has spent an inordinate amount of time on maintaining dependencies and packaging hacks to ensure that our tool, which now undergirds the reproducibility of millions of dollars worth of research studies, remains compatible with every platform. In the past couple of years, we estimate that we’ve spent hundreds of hours and over $250,000 of taxpayer-supported research funding engineering solutions to this problem. WheelNext would have solved this entirely, allowing us to focus our efforts on understanding and treating neurodegenerative diseases.
—Talmo Pereira, Ph.D., author of SLEAP and Principal Investigator at the Salk Institute for Biological Studies
The potential for improvement can be summarized as:
—Travis Oliphant, the author of NumPy and SciPy and Chief AI Architect at OpenTeams
This PEP presents the minimal scope required to meet modern heterogenous system needs. It leaves aspects beyond the minimal scope to evolve via tools or future PEPs. A non-exhaustive list of these aspects include:
This problem is not unique to the Python ecosystem, different groups and ecosystems have come up with various answers to that very problem. This section will focus on highlighting the strengths and weaknesses of the different approaches taken by various communities.
Conda is a binary-only package ecosystem that uses aggregated metadata indexes for resolution rather than filename parsing. Unlike the Simple repository API, conda’s resolution relies on repodata indexes per platform containing full metadata, making filenames purely identifiers with no parsing requirements.
Variant System: In 2016-2017, conda-build introduced variants to differentiate packages with identical name/version but different dependencies.
A hash (computed from variant metadata) prevents filename collisions; actual variant selection happens via standard dependency constraints in the solver. No special metadata parsing is needed—installers simply resolve dependencies like:
Mutex Metapackages: Python metadata and conda metadata do not have good ways to express ideas like “this package conflicts with that one.” The main mechanism for enforcement is sharing a common package name - only one package with a given name can exist at one time. Mutex metapackages are sets of packages with the same name, but different build string. Packages depend on specific mutex builds (e.g., blas=*=openblas vs blas=*=mkl) to avoid problems with related packages using different dependency libraries, such as NumPy using OpenBLAS and SciPy using MKL.
Example software variants: BLAS, MPI, OpenMP, noarch vs native
Virtual Packages: Introduced in 2019, virtual packages inject system detection (CUDA version, glibc, CPU features) as solver constraints. Built packages express dependencies like __cuda >=12.8, and the installer verifies compatibility at install time. Current virtual packages include archspec (CPU capabilities), OS/system libraries, and CUDA driver version. Detection logic is tool-specific (rattler, mamba).
archspec is a library for detecting, labeling, and reasoning about CPU microarchitecture variants, developed for the Spack package manager.
Variant Model: CPU Microarchitectures (e.g., haswell, skylake, zen2, armv8.1a) form a Directed Acyclic Graph (DAG) encoding binary compatibility, which helps at resolve to express that packageB depends on packageA. The ordering is partial because (1) separate ISA families are incomparable, and (2) contemporary designs may have incompatible feature sets—cascadelake and cannonlake are incomparable despite both descending from skylake, as each has unique AVX-512 extensions.
Implementation: A language-agnostic JSON database stores microarchitecture metadata (features, compatibility relationships, compiler-specific optimization flags). Language bindings provide detection (queries /proc/cpuinfo, matches to microarchitecture with largest compatible feature subset) and compatibility comparison operators.
Package Manager Integration: Spack records target microarchitecture as package provenance (spack install fftw target=broadwell), automatically selects compiler flags, and enables microarchitecture-aware binary caching. The European Environment for Scientific Software Installations (EESSI) distributes optimized builds in separate subdirectories per microarchitecture (e.g., x86_64, armv8.1a, haswell); runtime initialization uses archspec to select best compatible build when no exact match exists.
Gentoo Linux is a source-first distribution with support for extensive package customization. This is primarily achieved via USE flags: boolean flags exposed by individual packages and permitting fine-tuning the enabled features, optional dependencies and some build parameters (e.g. jpegxl for JPEG XL image format support, cpu_flags_x86_avx2 for AVX2 instruction set use). Flags can be toggled individually, and separate binary packages can be built for different sets of flags. The package manager can either pick a binary package with matching configuration or build from source.
API and ABI matching is primarily done through use of slotting. Slots are generally used to provide multiple versions or variants of given package that can be installed alongside (e.g. different major GTK+ or LLVM versions, or GTK+3 and GTK4 builds of WebKitGTK), whereas subslots are used to group versions within a slot, usually corresponding to the library ABI version. Packages can then declare dependencies bound to the slot and subslot used at build time. Again, separate binary packages can be built against different dependency slots. When installing a dependency version falling into a different slot or subslot, the package manager may either replace the package needing that dependency with a binary packages built against the new slot, or rebuild it from source.
Normally, the use of slots assumes that upgrading to the newest version possible is desirable. When more fine-grained control is desired, slots are used in conjunction with USE flags. For example, llvm_slot_{major} flags are used to select a LLVM major version to build against.
Wheel variants introduce a more fine-grained specification of built wheel characteristics beyond what existing wheel tags provide. Individual wheels carry a human-readable label defined at build time, as described in modified wheel filename, and are characterizing using variant property system. The properties are organized into a hierarchical structure of namespaces, features and feature values. When evaluating wheels to install, the installer determines whether variant properties of a given wheel are compatible with the system, and perform variant ordering based on the priority of the compatible variant properties. This is done in addition to determining the compatibility. The ordering by variant properties takes precedence over ordering by tags.
Every variant namespace is governed by a variant provider. There are two kinds of variant providers: install-time providers and ahead-of-time (AoT) providers. Install-time providers require plugins that are queried while installing wheels to determine the set of supported properties and their preference order. For AoT providers, this data is static and embedded in the wheel; it can be either provided directly by the wheel maintainer or queried at wheel build time from an AoT plugin.
Both kinds of plugins are usually implemented as Python packages which implement the provider plugin API, but they may also be vendored or reimplemented by installers to improve user experience, as outlined in Providers. Plugin packages may be installed in isolated or non-isolated environments. In particular, all plugins may be returned by the get_requires_for_build_wheel() hook of a PEP 517 backend, and therefore installed along with other build dependencies. For this reason, it is important that plugin packages do not narrowly pin dependencies, as that could prevent different packages from being installed simultaneously in the same environment.
Metadata governing variant support is defined in pyproject.toml file, and it is copied into variant.json file in wheels, as explored in metadata in source tree and wheels. Additionally, variant environment markers can be used to define dependencies specific to a subset of variants.
One of the core requirements of the design is to ensure that installers predating this PEP will ignore wheel variant files. This makes it possible to publish both variant wheels and non-variant wheels on a single index, with installers that do not support variants securely ignoring the former, and falling back to the latter.
A variant label component is added to the filename for the twofold purpose of providing a unique mapping from the filename to a set of variant properties, and providing a human-readable identification for the variant. The label is kept short and lowercase to avoid issues with different filesystems. It is added as a --separated component at the end to ensure that the existing filename validation algorithms reject it:
This behavior was confirmed for a number of existing tools: auditwheel, packaging, pdm, pip, poetry, and uv.
Variant properties serve the purpose of expressing the characteristics of the variant. Unlike platform compatibility tags, they are stored in the variant metadata and therefore do not affect the wheel filename length. They follow a hierarchical key-value design, with the key further broken into a namespace and a feature name. Namespaces are used to group features defined by a single provider, and to avoid conflicts should multiple providers define a feature with the same name. This permits independent governance and evolution of every namespace.
The keys are restricted to lowercase letters, digits, and underscores. Uppercase characters are disallowed to avoid different spellings of the same name. The character set for values is more relaxed, to permit values resembling versions.
Variant properties are serialized into a structured 3-tuple format inspired by Trove Classifiers in PEP 301:
Properties are used both to determine variant wheel compatibility, and to select the best variant to install. Provider plugins indicate which variant properties are compatible with the system, and order them by importance. This ordering can further be altered in variant wheel metadata.
Variant features can be declared as allowing multiple values to be present within a single variant wheel. If that is the case, these values are matched as a logical OR, i.e. only a single value needs to be compatible with the system for the wheel to be considered supported. On the other hand, features are treated as a logical AND, i.e. all of them need to be compatible. This provides some flexibility in designating variant compatibility while avoiding having to implement a complete boolean logic.
Typically, variant features will be single-value and indicate minimal or mutually exclusive requirements. The system may indicate multiple compatible values. For example, if the feature declares a minimum CUDA runtime version, the provider will indicate compatibility with wheels requiring a minimum version corresponding to the currently installed version or older, e.g. for CUDA 12.8, the compatible minimum versions used in wheels would be, in order of decreasing preference:
Similarly, a wheel could indicate its minimum required CPU version, and the provider will indicate all the compatible CPU versions.
Multi-value features are useful for “fat” packages where multiple incompatible targets are supported by a single package. A typical example are GPUs. In this case, the wheel declares a number of supported GPUs, and the provider indicates which GPUs are actually installed (usually one). The wheel is compatible if there is overlap between the two lists.
A null variant is a variant wheel with no properties, but distinct from non-variant wheels in having the null variant label and variant metadata. During the transition period, it provides the possibility of providing a distinct fallback for systems that do not support any of the variants provided, and for systems that do support variant wheels at all.
For example, a package with optional GPU support could publish three kinds of wheels:
Publishing a null variant is optional, and makes sense only if distinct fallbacks provide advantages to the user. If one is published, a wheel variant-enabled installer will prefer it over the non-variant wheel. If it is not, it will fall back to the non-variant wheel instead. The non-variant wheel is also used if variant support is explicitly disabled by an installer flag.
The null variant uses a reserved null label to make it clearly distinguishable from regular variants.
The variant wheel metadata specifies what providers are used for its properties. Providers serve a twofold purpose:
The specification proposes two kinds of providers: install-time providers and Ahead-of-Time providers.
Install-time providers are implemented either as Python packages that need to be installed and run to query them, or vendored or reimplemented in the tools. They are used when user systems need to be queried to determine wheel compatibility, for example for variants utilizing GPUs or requiring CPU instruction sets beyond what platform tags provide. Installing third-party packages involves security risks highlighted in the security implications section, and the proposed mitigations incur a cost on installer implementations.
Ahead-of-Time providers are implemented as static metadata embedded in the wheel. They are used when particular variant properties are always compatible with the user’s system (provided that a wheel using them has been built successfully). However, the metadata indicates which properties are preferred. For example, AoT providers can be used to provide choice between builds against different BLAS / LAPACK providers, or to provide debug builds of packages. Since they do not require running code external to the installer, they do not pose the problems faced by install-time providers, and can be used more liberally.
AoT providers are permitted to feature plugin packages. If that is the case, these packages are only used when building wheels, and their output is used to fill in the static metadata used at install time. This way, it is easier to use consistent property names and values across multiple packages. Otherwise, the package maintainer needs to include the supported properties directly in the pyproject.toml file.
When implemented as Python packages, both kinds of provider plugins expose roughly the same API. However, an AoT provider must always consider all valid variant properties supported, and it must always return the same ordered list of supported properties irrespective of the user system. All AoT providers can technically be used as install-time providers, but not the other way around.
As the specification introduces the potential necessity of installing and running provider packages to install wheels, it is recommended that these packages remain functioning correctly for the variant wheels published in the past, including very old package versions. Ideally, no properties previously supported should ever be removed.
If a breaking change needs to be performed, it is recommended to either introduce a new provider package for that, or add a new plugin API endpoint to the existing package. In both cases, it may be necessary to preserve the old endpoint in minimal maintenance mode, to ensure that old wheels can still be installed. The old endpoint can trigger deprecation warnings in the get_all_configs() hook that is used when building packages.
An alternative approach is to use semantic versioning to cut off breaking changes. However, this relies on package authors reliably using caps on dependencies, as otherwise old wheels will start using incompatible plugin versions. This is already a problem with Python build backends used today.
When vendoring or reimplementing plugins, installers need to follow their current behavior. In particular, they should recognize the relevant provider versions numbers, and possibly fall back to installing the external plugin when the package in question is incompatible with the installer’s implementation.
Variants introduce a few new portions of metadata that are stored in the source tree and in wheels. In the source tree, it is stored in the pyproject.toml file along with other project properties, benefiting from the TOML format’s readability and strictness. Afterwards, it is converted into an equivalent JSON structure, and stored as a separate file in the .dist-info directory. The existing metadata files are unchanged to avoid unnecessary incompatibility, and to avoid serializing into the inconvenient Core Metadata format.
The metadata in pyproject.toml includes:
In wheel metadata, the above is amended by static property lists obtained from the plugins and variant properties for the built wheel.
When wheels are published on an index, the variant metadata from all wheels is combined into a single {name}-{version}-variants.json file that is used by clients to efficiently obtain the variant metadata without having to download it from individual wheels separately, or implement explicit variant metadata support in an API provided by the package index server.
Some packages provide extension modules exposing an Application Binary Interface (ABI) that is not compatible across wide ranges of versions. The packages using this interface need to pin their wheels to the version used at build time. If ABI changes frequently, the pins are very narrow and users face problems if they need to install two packages that may happen to pin to different versions of the same dependency. Providing variants built against different dependency versions can increase the chance of a resolver being able to find a dependency version that is compatible with all the packages being installed.
Unfortunately, such a variant provider cannot be implemented within the plugin API defined by the specification. Given that a robust implementation would need to interface with the dependency resolver, rather than attempt to extend the API to cover this use case and add significant complexity as a result, the specification reserves abi_dependency as a special variant namespace that can be implemented by installers wishing the provide this feature.
Given the complexity of the problem, this extension is made entirely optional. This implies that any packages using it need to provide non-variant wheels as well.
A conceptual diagram of installing a wheel.
When asked to install a version of a package from an index, the proposed tool behavior would be to:
When asked to install a local wheel file, the tool’s proposed behavior would be to:
In order to build a variant wheel, the build backend needs to receive a list of variant properties and a variant label. The recommended way to do that is to use backend-defined keys in the config_settings dictionary passed to the build backend hooks.
When building a variant wheel, the proposed behavior for the build backend would be to:
Variant wheels are uploaded to an index just like regular wheels. There are two possible approaches to publishing the index-level {name}-{version}-variants.json file for every package version: it can either be prepared and uploaded by the user, or it can be generated automatically by the index.
The file should not be changed once it is published, as clients may have already cached it or locked to the existing hash. For this reason, if the index is responsible for generating the file, it should use some mechanism to defer publishing it until the release is fully uploaded (for example, PEP 694).
To generate the {name}-{version}-variants.json file:
As of October 2025, PyTorch publishes a total of seven variants for every release: a CPU-only variant, three CUDA variants with different minimal CUDA runtime versions and supported GPUs, two ROCm variants and a Linux XPU variant.
This setup could be improved using GPU/XPU plugins that query the installed runtime version and installed GPUs/XPUs to filter out the wheels for which the runtime is unavailable, it is too old or the user’s GPU is not supported, and order the remaining variants by the runtime version. The CPU-only version is published as a null variant that is always supported.
If a GPU runtime is available and supported, the installer automatically chooses the wheel for the newest runtime supported. Otherwise, it falls back to the CPU-only variant. In the corner case when multiple accelerators are available and supported, PyTorch package maintainers indicate which one takes preference by default.
Wheel variants can be used to provide variants requiring specific CPU extensions, beyond what platform tags currently provide. They can be particularly helpful when runtime dispatching is impractical, when the package relies on prebuilt components that use instructions above the baseline, when availability of instruction sets implies library ABI changes, or simply to benefit from compiler optimizations such as auto-vectorization applied across the code base.
For example, an x86-64 CPU plugin can detect the capabilities for the installed CPU, mapping them onto the appropriate x86-64 architecture level and a set of extended instruction sets. Variant wheels indicate which level and/or instruction sets are required. The installer filters out variants that do not meet the requirements and select the best optimized variant. A non-variant wheel can be used to represent the architecture baseline, if supported.
Implementation using wheel variants makes it possible to provide fine-grained indication of instruction sets required, with plugins that can be updated as frequently as necessary. In particular, it is neither necessary to cover all available instruction sets from the start, nor to update the installers whenever the instruction set coverage needs to be improved.
Packages such as NumPy and SciPy can be built using different BLAS / LAPACK libraries. Users may wish to choose a specific library for improved performance on a particular hardware, or based on license considerations. Furthermore, different libraries may use different OpenMP implementations, whereas using a consistent implementation across the stack can avoid degrading performance through spawning too many threads.
BLAS / LAPACK variants do not require a plugin at install time, since all variants built for a particular platform are compatible with it. Therefore, an ahead-of-time provider (with install-time = false) that provides a predefined set of BLAS / LAPACK library names can be used. When the package is installed, normally the default variant is used, but the user can explicitly select another one.
A package may wish to provide a special debug-enabled builds for debugging or CI purposes, in addition to the regular release build. For this purpose, an optional ahead-of-time provider can be used (install-time = false with optional = true), defining a custom property for the debug builds. Since the provider is disabled by default, users normally install the non-variant wheel providing the release build. However, they can easily obtain the debug build by enabling the optional provider or selecting the variant explicitly.
Packages such as vLLM need to be pinned to the PyTorch version they were built against to preserve Application Binary Interface (ABI) compatibility. This often results in unnecessarily strict pins in package versions, making it impossible to find a satisfactory resolution for an environment involving multiple packages requiring different versions of PyTorch, or resorting to source builds. Variant wheels can be used to publish variants of vLLM built against different PyTorch versions, therefore enabling upstream to easily provide support for multiple versions simultaneously.
The optional abi_dependency extension can be used to build multiple vllm variants that are pinned to different PyTorch versions, e.g.:
The proposal introduces a plugin system for querying the system capabilities in order to determine variant wheel capability. The system permits specifying additional Python packages providing the plugins in the package index metadata. Installers and other tools that need to determine whether a particular wheel is installable, or select the most preferred variant among multiple variant wheels, may need to install these packages and execute the code within them while resolving dependencies or processing wheels.
This elevates the supply-chain attack potential by introducing two new points for malicious actors to inject arbitrary code payload:
While such attacks are already possible at the package dependency level, it needs to be emphasized that in some scenarios the affected tools are executed with elevated privileges, e.g. when installing packages for multi-user systems, while the installed packages are only used with regular user privileges afterwards. Therefore, variant provider plugins could introduce a Remote Code Execution vulnerability with elevated privileges.
A similar issue already exists in the packaging ecosystem when packages are installed from source distributions, whereas build backends and other build dependencies are installed and executed. However, various tools operating purely on wheels, as well as users using tool-specific options to disable use of source distributions, have been relying on the assumption that no code external to the system will be executed while resolving dependencies, installing a wheel or otherwise processing it. To uphold this assumption, the proposal explicitly requires that untrusted provider plugin packages are never installed without explicit user consent.
The Providers section of the specification provides further suggestions that aim to improve both security and the user experience. Particularly, it is expected that the most popular provider plugins will be available out of the box, and a dedicated team of maintainers (initially including a subset of the PEP authors) will be responsible for inspecting them for security risks and vetting the plugins as safe to use. Installers will be able to either use a published allowlist, vendor specific provider plugin versions, reimplement them or use them as a Python library at their leisure.
This will lead to the majority of packages focusing on these specific plugins, rather than implementing competing solutions. Plugins requiring explicit opt-in should be rare, and primarily affect expert users. This is important to make variant usage secure-by-default. Furthermore, the frequent disruption of workflows incentivises users to blanket-allow all plugins (security fatigue).
Furthermore, the specification permits using static configuration as input to skip running plugins altogether.
This PEP proposes a set of extensions to the Binary distribution format specification that enable building additional variants of wheels that can be installed by variant-aware tools while being ignored by programs that do not implement this specification.
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.
The wheel filename template originally defined by PEP 427 is changed to:
Wheels using extensions introduced by this PEP MUST feature the variant label component. The label MUST adhere to the following rules:
This is equivalent to the following regular expression: ^[0-9a-z._]{1,16}$.
Every label MUST uniquely correspond to a specific set of variant properties, which MUST be the same for all wheels using the same label within a single package version. Variant labels SHOULD be specified at wheel build time, as human-readable strings. The label null is reserved for the null variant and MUST use an empty set of variant properties.
Installers that do not implement this specification MUST ignore wheels with variant label when installing from an index, and fall back to a wheel without such label if it is available. If no such wheel is available, the installer SHOULD output an appropriate diagnostic, in particular warning if it results in selecting an earlier package version or a clear error if no package version can be installed.
Examples:
Every variant wheel MUST be described by zero or more variant properties. A variant wheel with exactly zero properties represents the null variant. The properties are specified when the variant wheel is being built, using a mechanism defined by the project’s build backend.
Each variant property is described by a 3-tuple that is serialized into the following format:
The namespace MUST consist only of 0-9, a-z and _ ASCII characters (^[a-z0-9_]+$). It MUST correspond to a single variant provider.
The feature name MUST consist only of 0-9, a-z and _ ASCII characters (^[a-z0-9_]+$). It MUST correspond to a valid feature name defined by the respective variant provider in the namespace.
The feature value MUST consist only of 0-9, a-z, _ and . ASCII Characters (^[a-z0-9_.]+$). It MUST correspond to a valid value defined by the respective variant provider for the feature.
If a feature is marked as “multi-value” by the provider plugin, a single variant wheel can define multiple properties sharing the same namespace and feature name. Otherwise, there MUST NOT be more than a single value corresponding to a single pair of namespace and feature name within a variant wheel.
For a variant wheel to be considered compatible with the system, all of the features defined within it MUST be determined to be compatible. For a feature to be compatible, at least a single value corresponding to it MUST be compatible.
Examples:
When installing or resolving variant wheels, installers SHOULD query the variant providers to verify whether a given wheel’s properties are compatible with the system and to select the best variant through variant ordering. However, they MAY provide an option to omit the verification and install a specified variant explicitly.
Providers can be marked as install-time or ahead-of-time. For install-time providers, installers MUST either query the provider for variant property compatibility, or use user-provided compatibility information. Installers MAY vendor or reimplement specific providers. The format of user-provided information is left implementation-defined.
For ahead-of-time providers, they MUST use the static metadata embedded in the wheel instead.
Providers can be marked as optional. If a provider is marked optional, then the installer MUST NOT query said provider by default, and instead assume that its properties are incompatible. It SHOULD provide an option to enable optional providers.
Providers can also be made conditional to Environment Markers. If that is the case, the installer MUST check the markers against the environment to which wheels are going to be installed. It MUST NOT use any providers whose markers do not match, and instead assume that their properties are incompatible.
All the tools that need to query variant providers and are run in a security-sensitive context, MUST NOT install or run provider packages, unless they can determine the particular provider package version to be trusted. The exact mechanism used to do that is implementation-specific. However, installers SHOULD ensure that the most commonly used providers can be securely used without an explicit user opt-in.
When installing provider packages, tools SHOULD use an isolated virtual environment.
Install-time provider packages SHOULD take measures to guard against supply chain attacks, for example by vendoring all dependencies.
For a consistent experience between tools, variant wheels SHOULD be supported by default. Tools MAY provide an option to only use non-variant wheels.
This section describes the metadata format for the providers, variants and properties of a package and its wheels. The format is used in three locations, with slight variations:
All three variants metadata files share a common JSON-compatible structure:
The top-level object is a dictionary rooted at a specific point in the containing file. Its individual keys are sub-dictionaries that are described in the subsequent sections, along with the requirements for their presence. The tools MUST ignore unknown keys in the dictionaries for forwards compatibility of updates to the PEP. However, users MUST NOT use unsupported keys to avoid potential future conflicts.
A JSON schema is included in the Appendix of this PEP, to ease comprehension and validation of the metadata format. This schema will be updated with each revision to the variant metadata specification. The schema is available in Appendix: JSON Schema for Variant Metadata.
Ultimately, the variant metadata JSON schema SHOULD be served by packaging.python.org.
providers is a dictionary, the keys are namespaces, the values are dictionaries with provider information. It specifies how to install and use variant providers. A provider information dictionary MUST be declared in pyproject.toml for every variant namespace supported by the package. It MUST be copied to variant.json as-is, including the data for providers that are not used in the particular wheel.
The use of provider information is described in the Providers and Provider plugin API sections.
A provider information dictionary MAY contain the following keys:
All the fields are OPTIONAL, with the following exceptions:
The default-priorities dictionary controls the ordering of variants. The exact algorithm is described in the Variant ordering section.
It has a single REQUIRED key:
It MAY have the following OPTIONAL keys:
The static-properties dictionary specifies the supported properties for AoT providers. It is a nested dictionary with namespaces as first level keys, feature name as second level keys and ordered lists of feature values as second level values.
In pyproject.toml file, the namespaces present in this dictionary MUST correspond to all AoT providers without a plugin (i.e. with install-time of false and no or empty requires). When building a wheel, the build backend MUST query the AoT provider plugins (i.e. these with install-time being false and non-empty requires) to obtain supported properties and embed them into the dictionary. Therefore, the dictionary in variant.json and *-variants.json MUST contain namespaces for all AoT providers (i.e. all providers with install-time being false).
Since TOML and JSON dictionaries are unsorted, so are the features in the static-properties dictionary. If more than one feature is specified for a namespace, then the order for all features MUST be specified in default-priorities.feature.{namespace}. If an AoT plugin is used to fill static-properties, then the features not already in the list in pyproject.toml MUST be appended to it.
The list of values is ordered from the most preferred to the least preferred, same as the lists returned by get_supported_configs() plugin API call (as defined in plugin interface). The default-priorities.property dict can be used to override the property ordering.
The variants dictionary is used in variant.json to indicate the variant that the wheel was built for, and in *-variants.json to indicate all the wheel variants available. It’s a 3-level dictionary listing all properties per variant label: The first level keys are variant labels, the second level keys are namespaces, the third level are feature names, and the third level values are lists of feature values.
The pyproject.toml file is the standard project configuration file as defined in pyproject.toml specification. The variant metadata MUST be rooted at a top-level table named variant. It MUST NOT specify the variants dictionary. It is used by build backends to build variant wheels.
Example Structure:
The variant.json file MUST be present in the *.dist-info/ directory of a built variant wheel. It is serialized into JSON, with the variant metadata dictionary being the top object. It MUST include all the variant metadata present in pyproject.toml, copied as indicated in the individual key sections. In addition to that, it MUST contain:
The variant.json file corresponding to the wheel built from the example pyproject.toml file for x86-64-v3 would look like:
For every package version that includes at least one variant wheel, there MUST exist a corresponding {name}-{version}-variants.json file, hosted and served by the package index. The {name} and {version} placeholders correspond to the package name and version, normalized according to the same rules as wheel files, as found in the File name convention of the Binary Distribution Format specification. The link to this file MUST be present on all index pages where the variant wheels are linked. It is presented in the same simple repository format as source distribution and wheel links in the index, including an (OPTIONAL) hash.
This file uses the same structure as variant.json described above, except that the variants object MUST list all variants available on the package index for the package version in question. It is RECOMMENDED that tools enforce the same contents of the default-priorities, providers and static-properties sections for all variants listed in the file, though careful merging is possible, as long as no conflicting information is introduced, and the resolution results within a subset of variants do not change.
The index MAY generate the index level variant metadata file automatically from uploaded wheel metadata. If that is the case, the file SHOULD NOT be published until it is final, and once published, it SHOULD NOT change, as clients MAY cache it.
If the file is not generated automatically, the index MUST permit package maintainers to upload it. Once the variant level metadata file is uploaded, the package maintainers SHOULD NOT upload new variants for the version in question.
The foo-1.2.3-variants.json corresponding to the package with two wheel variants, one of them listed in the previous example, would look like:
To determine which variant wheel to install when multiple wheels are compatible, variant wheels MUST be ordered by their variant properties.
For the purpose of ordering, variant properties are grouped into features, and features into namespaces. The ordering MUST be equivalent to the following algorithm:
After this step, a list of ordered feature names is available for every namespace.
After this step, a list of ordered property values is available for every feature.
After this process, the variant wheels are sorted from the most preferred to the least preferred. The null variant naturally sorts after all the other variants, and the non-variant wheel MUST be sorted after the null variant. Multiple wheels with the same variant set (and multiple non-variant wheels) MUST then be ordered according to their platform compatibility tags.
Alternatively, the sort algorithm for variant wheels could be described using the following pseudocode. For simplicity, this code does not account for non-variant wheels or tags.
The following section is added to the pylock.toml Specification:
If there is a [packages.variants-json] section, the installer SHOULD resolve variants to select the best wheel file.
Every provider plugin MUST operate within a single namespace. This namespace is used as a unique key for all plugin-related operations. All the properties defined by the plugin are bound within the plugin’s namespace, and the plugin defines all the valid feature names and values within that namespace.
Provider plugin authors SHOULD choose namespaces that can be clearly associated with the project they represent, and avoid namespaces that refer to other projects or generic terms that could lead to naming conflicts in the future.
All variants published on a single index for a specific package version MUST use the same provider for a given namespace. Attempting to load more than one plugin for the same namespace in the same release version MUST result in a fatal error. While multiple plugins for the same namespace MAY exist across different packages or release versions (such as when a plugin is forked due to being unmaintained), they are mutually exclusive within any single release version.
To make it easier to discover and install plugins, they SHOULD be published in the same indexes that 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, installable Python packages MUST be provided for plugins. However, as noted in the Providers section, these plugins can also be reimplemented by tools needing them. In the latter case, the resulting reimplementation does not need to follow the API defined in this section.
Plugin packages may be run in an isolated environment. They MUST NOT make decisions based on installed packages.
A plugin implemented as Python package exposes two kinds of objects at a specified API endpoint:
These can be implemented either as modules, or classes with class methods or static methods. The specifics are provided in the subsequent sections.
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:
An API endpoint specification is equivalent to the following Python pseudocode:
API endpoints are used in two contexts:
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. It is defined using the following protocol:
A “variant feature config” MUST provide the following properties or attributes:
All features are interpreted as being within the plugin’s namespace.
The plugin interface MUST follow the following protocol:
The plugin interface MUST define the following attributes:
The plugin interface MUST provide the following functions:
The value returned by get_supported_configs() MUST be a subset of the feature names and values returned by get_all_configs() (modulo ordering).
The value returned by get_supported_configs() MAY be cached throughout multiple packages in a single install session.
The future versions of this specification, as well as third-party extensions MAY introduce additional properties and methods on the plugin instances. The implementations SHOULD ignore additional attributes.
For best compatibility, all private attributes SHOULD be prefixed with an underscore (_) character to avoid incidental conflicts with future extensions.
As a build backend can’t determine whether the frontend supports variant wheels or not, PEP 517 and PEP 660 hooks MUST build non-variant wheels by default. Build backends MAY provide ways to request variant builds. This specification does not define any specific configuration.
When building variant wheels, build backends MUST verify variant metadata for correctness, and they MUST NOT emit wheels with nonconformant variant.json files. They SHOULD also query providers to determine whether variant properties requested by the user are valid, though they MAY permit skipping this verification and therefore emitting variant wheels with potentially unknown properties.
Four new environment markers are introduced in dependency specifications:
The markers evaluating to sets of strings MUST be matched via the in or not in operator, e.g.:
The variant_label marker is a plain string:
Implementations MUST ignore differences in whitespace while matching the features and properties.
Variant marker expressions MUST be evaluated against the variant properties stored in the wheel being installed, not against the current output of the provider plugins. If a non-variant wheel was selected or built, all variant markers evaluate to False.
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 used by any variant provider plugin, it MUST NOT be listed in providers metadata, and can only appear in a built wheel variant property.
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.
| 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.:
This means the wheel is compatible with both PyTorch 2.8.0 and 2.9.0.
The primary source of information for Python package users should be installer documentation, supplemented by helpful informational messages from command-line interface, and tutorials. Users without special needs should not require any special variant awareness. Advanced users would specifically need documentation on (provided the installer in question implements these features):
The installer documentation may also be supplemented by documentation specific to Python projects, in particular their installation instructions.
For the transition period, during which some package managers do and some do not support variant wheels, users need to be aware that certain features may only be available with certain tools.
The primary source of information for maintainers of Python packages should be build backend documentation, supplemented by tutorials. The documentation needs to indicate:
The maintainers will also need to peruse provider plugin documentation. They should also be aware which provider plugins are considered trusted by commonly used installers, and know the implications of using untrusted plugins. These materials may also be supplemented by generic documents explaining publishing variant wheels, along with specific example use cases.
For the transition period, package maintainers need to be aware that they should still publish non-variant wheels for backwards compatibility.
Existing installers MUST NOT accidentally install variant wheels, as they require additional logic to determine whether a wheel is compatible with the user’s system. This is achieved by extending wheel filename through adding a -{variant label} component to the end of the filename, effectively causing variant wheels to be rejected by common installer implementations. For backwards compatibility, a non-variant wheel can be published in addition to the variant wheels. It will be the only wheel supported by incompatible installers, and the least preferred wheel for variant-compatible installers.
Aside from this explicit incompatibility, the specification makes minimal and non-intrusive changes to the binary package format. The variant metadata is placed in a separate file in the .dist-info directory, which should be preserved by tools that are not concerned with variants, limiting the necessary changes to updating the filename validation algorithm (if there is one).
If the new variant environment markers are used in wheel dependencies, these wheels will be incompatible with existing tools. This is a general problem with the design of environment markers, and not specific to wheel variants. It is possible to work around this problem by partially evaluating environment markers at build time, and removing the markers or dependencies specific to variant wheels from the non-variant wheel.
Build backends produce non-variant wheels to preserve backwards compatibility with existing frontends. Variant wheels can only be output on explicit user request.
By using a separate *-variants.json file for shared metadata, it is possible to use variant wheels on an index that does not specifically support variant metadata. However, the index MUST permit distributing wheels that use the extended filename syntax and the JSON file.
The variantlib project contains a reference implementation of all the protocols and algorithms introduced in this PEP, as well as a command-line tool to convert wheels, generate the *-variants.json index and query plugins.
A client for installing variant wheels is implemented in a uv branch.
The Wheel Variants monorepo includes example implementations of provider plugins, as well as modified versions of build backends featuring variant wheel building support and modified versions of some Python packages demonstrating variant wheel uses.
In discussing the security concerns, proposals were made to make variant provider usage entirely opt-in, either permanently, or at least initially to facilitate further testing. While such approaches may alter who takes responsibility of vetting the provider code, and hence the maintenance effort or number of packages/maintainers that need to be trusted, they are not suitable as long-term solutions.
Most importantly, the opt-in mechanism would lead to far worse user experience out-of-the-box. For variant-enabled packages, the default experience would be installing a suboptimal or outright broken variant. It should be noted that variant-enabled packages may not only be installed directly, but also as dependencies of other packages. Therefore, for optimal user experience, all packages that are variant-enabled or that feature dependencies that are variant-enabled, would have to document appropriate installer-specific mechanisms for enabling the respective provider plugins.
The proliferation of this experience could have two significant outcomes. The inability to make variants work out of the box for users could lead to package maintainers refraining from using them, and instead sticking to the earlier workarounds. What’s even worse, it could also lead to users eventually naively configuring their installers to enable all variant providers unconditionally, effectively rendering the provider usage opt-out for a large number of users, enabling all kinds of supply chain attacks described in the security implications section.
The authors would like to emphasize that security cannot be achieved at the cost of severely impaired user experience. Instead, the PEP attempts to strike a balance by introducing a centrally maintained and vetted pool of trusted providers.
The support for additional variant properties could technically be implemented without introducing provider plugins, but rather defining the available properties and their discovery methods as part of the specification, much like how wheel tags are implemented currently. However, the existing wheel tag logic already imposes a significant complexity on packaging tools that need to maintain the logic for generating supported tags, partially amortized by the data provided by the Python interpreter itself.
Every new axis would be imposing even more effort on package manager maintainers, who would have to maintain an algorithm to determine the property compatibility. This algorithm could become quite complex, possibly needing to account for different platforms, hardware versions and requiring more frequent updates than the one for platform tags. This would also significantly increase the barrier towards adding new axes and therefore the risk of lack of feature parity between different installers, as every new axis will be imposing additional maintenance cost.
For comparison, the plugin design essentially democratizes the variant properties. Provider plugins can be maintained independently by people having the necessary knowledge and hardware. They can be updated as frequently as necessary, independently of package managers. The decision to use a particular provider falls entirely on the maintainer of package needing it, though they need to take into consideration that using plugins that are not vetted by the common installers will inconvenience their users.
An alternative proposal was to publish the variants of the package as separate projects on the index, along with the main package serving as a “resolver” directing to other variants via its metadata. For example, a torch package could indicate the conditions for using torch-cpu, torch-cu129, etc. subpackages.
Such an approach could possibly feature better backwards compatibility with existing tools. The changes would be limited to installers, and even with pre-variant installers the users could explicitly request installing a specific variant. However, it poses problems at multiple levels.
The necessity of creating a new project for every variant will lead to the proliferation of old projects, such as torch-cu123. While the use of resolver package will ensure that only the modern variants are used, users manually installing packages and cross-package dependencies may accidentally be pinning to old variant projects, or even fall victim to name squatting. For comparison, the variant wheel proposal scopes variants to each project version, and ensures that only the project maintainers can upload them.
Furthermore, it requires significant changes to the dependency resolver and package metadata formats. In particular, the dependency resolver would need to query all “resolver” packages before performing resolution. It is unclear how to account for such variants while performing universal resolution. The one-to-one mapping between dependencies and installed packages would be lost, as a torch dependency could effectively be satisfied by torch-cu129.
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.
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.
Source: https://github.com/python/peps/blob/main/peps/pep-0817.rst
Last modified: 2026-07-01 18:24:41 UTC