Python’s existing wheel packaging format usesPlatform compatibility tags to specifya given wheel’s supported environments. These tags are unable to expressmodern hardware configurations and their features, such as the availability ofGPU acceleration. The tags fail to provide custom package variants, such as buildsagainst different dependency ABIs. These inabilities are particularly challenging forscientific computing, artificial intelligence (AI), machine learning(ML), and high-performance computing (HPC) communities.
This PEP proposes “Wheel Variants”, an extension to theBinary distribution format. Thisextension introduces a mechanism for package maintainers to declaremultiple build variants for the same package version, while allowinginstallers to automatically select the most appropriate variant based onsystem hardware and software characteristics. More specifically, itproposes:
An evolution of the wheel format calledWheel Variant that allowswheels to be distinguished by hardware or software attributes.
Avariant provider plugin interface that allows installers todynamically detect platform attributes and select the most suitablewheel.
The goal is for the obvious installation commands ({tool}install<package>)to select the most appropriate wheel, and provide the best user experience.
The2024 Python Developers Survey shows that a significantproportion of Python’s users have scientific computing use-cases. Thisincludes data analysis (40% of respondents), machine learning (30%), anddata engineering (30%). Many of the software packages developed forthese areas rely on diverse hardware features that cannot be adequatelyexpressed in the current wheel format, as highlighted inthelimitations of platform compatibility tags.
For example, packages such asPyTorch need tobe built for specific CUDA or ROCm versions, and that information cannotcurrently be included in the wheel tag. Having to build multiple wheelstargeting very different hardware configurations forces maintainers intovarious distribution strategies that are suboptimal, and create frictionfor users and authors of other software who wish to depend on thepackage in question.
A few existing approaches are explored inCurrent workarounds and theirdrawbacks. They include maintaining separate package indexes fordifferent hardware configurations, bundling all potential variants intoa single wheel of considerable size, or using separate package names(mypackage-gpu,mypackage-cpu, etc.). Each of these approacheshas significant drawbacks and potential security implications.
The current wheel format encodes compatibility through three platformtags:
Python tag: encoding the minimum Python version and optionallyrestricting Python distributions (e.g.,py3 for any Python 3,py313 for Python 3.13 or newer,cp313 for specificallyCPython, 3.13 or newer).
ABI tag: encoding the Python ABI required by any extensionmodules (e.g.,none for no requirement,abi3 for the CPythonstable ABI,cp313 for extensions requiring CPython 3.13 ABI).
Platform tag: currently encoding the operating system,architecture and core system libraries (e.g.,any for anyplatform,manylinux_2_34_x86_64 for x86-64 Linux system withglibc 2.34 or newer,macosx_14_0_arm64 for arm64 macOS 14.0or newer system.
These tags are limited to expressing the most fundamental propertiesof the Python interpreter, operating system and the broad CPUarchitectures. They cannot express anything more detailed, includingnon-CPU hardware requirements or library ABI constraints.
This lack of flexibility has led many projects to find sub-optimal - yetnecessary - workarounds, such as the manual installation commandselector provided by the PyTorch team. This complexity represents afundamental scalability issue with the current tag system that is notextensible enough to handle the combinatorial complexity of buildoptions.
Projects such asNumPy currently resort to building wheels for abaseline CPU target, and using runtime dispatching forperformance-critical routines. Such a solution requires additionaleffort from package maintainers, and usually doesn’t let the codebenefit from compiler optimizations outside the few select functions.
For comparison, buildingGROMACS forhigher CPU baselines proved to provide significant speedups:
Performance of GROMACS 2020.1 built for different generations ofCPUs. Vertical axis shows performance expressed in ns/day, aGROMACS-specific measure of simulation speed (higher is better).
CompilingGROMACS for architectures that can exploit the AVX-512instructions supported by the Intel Cascade Lake microarchitecturegives an additional 18% performance improvement relative to usingAVX2 instructions, with a speedup of about 70% compared to a genericGROMACS installation with only SSE2.
Projects such asPyTorchandRAPIDScurrently distribute packages that approximate “variants” throughseparate package indexes with custom URLs. We will use theexample of PyTorch, while the problem, the workarounds, and the impacton users also apply to other packages.
PyTorch uses a combination of index URLs per accelerator type and localversion segments as accelerator tag (such as+cu130,+rocm6.4 or+cpu) . Users need to first determine the correct index URL fortheir system, and add an index specifically for PyTorch.
Tools need to implement special handling for the way PyTorch uses localversion segments. These requirements break the pattern that packagesare usually installed with. Problems with installing PyTorchare a very common point of user confusion. To quantify this, on2025-12-05, 552 out of 8136 (6.8%), of issues onuv’s issue tracker contained the term “torch”.
Security Risk: This approach has unfortunately led to supplychain attacks - more details on thePyTorch Blog. It’s anon-trivial problem to address which has forced the PyTorch team tocreate a complete mirror of all their dependencies, and is one of thecore motivations behindPEP 766.
The complexity of configuration often leads to projects providing ad-hocinstallation instructions that do not provide for seamless packageupgrades.
Maintainers of other software cannot express that they depend on eitherof the available variants being selected. They need toeither depend on a specific variant, provide multiple alternativedependency sets using extras, or even publish their own software usingmultiple package names matching upstream variants.
Commonly, these packages install overlapping files. Since Pythonpackaging does not support expressing that two packages are mutuallyexclusive, installers can install both of them to the same environment,with the package installed second overwriting files from the oneinstalled first. This leads to runtime errors, andthe possibility of incidentally switching between variants depending onthe way package upgrades are ordered.
An additional limitation of this approach is that publishing a newrelease synchronously across multiple package names is not currentlypossible.PEP 694 proposes adding such a mechanism for multiplewheels within a single package, but extending it to multiple packages isnot a goal.
Security Risk: proliferation of suffixed variant packagesleads users to expect these suffixes in other packages, making namesquatting much easier. For example, one could create a maliciousnumpy-cuda package that users will be lead to believe it’s a CUDAvariant of NumPy.
As of the time of writing,CuPy has already registered a total of 55cupy* packages with different names, most of them never actuallyused (they are only visible through the use of Simple API), and a largepart of the remaining ones no longer updated. This clearly highlightsthe magnitude of the problem, and the effort put into countering therisk of name squatting.
JAX uses aplugin-based approach. The centraljax package provides a number ofextras that can be used to install additional plugins,e.g.jax[cuda12] orjax[tpu]. This is far from ideal aspipinstalljax (with no extra) leads to a nonfunctionalinstallation, and consequently dependency chains, a fundamental expectedbehavior in the Python ecosystem, are dysfunctional.
JAX includes 12 extras to cover all use cases - many of whichoverlap and could be misleading to users if they don’t read thedocumentation in detail. Most of them are technically mutuallyexclusive, though it is currently impossible to correctly express thiswithin the package metadata.
Including all possible variants in a single wheel is another option, butthis leads to excessively large artifacts, wasting bandwidth and leadingto slower installation times for users who only need one specificvariant. In some cases, such artifacts cannot be hosted on PyPI becausethey exceed its size limits.
FlashAttention doesnot publish wheels on PyPI at all, but instead publishes a customizedsource distribution that performs platform detection, downloads theappropriate wheel from an upstream server, and then provides it to theinstaller. This approach can select the optimal variant automatically,but it prevents binary-only installs from working, requires a slow anderror-prone build via a source distribution, and breaks common cachingassumptions tied to the wheel filename. It also requires a speciallyprepared build environment that contains thetorch package matchingthe version that the software will run against, which requires buildingwithout build isolation. On the project side, it requires hosting wheelsseparately.
Security Risk: Similar to regular source builds, thismodel requires running arbitrary code at install time. The wheelsare downloaded entirely outside the package manager’s control, extendingthe attack surface to two separate wheel download implementations andpreventing proper provenance tracking.
The packaging limitations particularly affect scientific computing andAI/ML applications where performance optimization is critical:
The current wheel format’s lack of hardware awareness creates asuboptimal experience for hardware-dependent packages. While pluginshelp with smaller and well scoped packages, users must currentlymanually identify the correct variant (e.g.,jax[cuda13]) toavoid generic defaults or incompatible combinations. We need asystem wherepipinstalljax automatically selects packagesmatching the user’s hardware, unless explicitly overridden.
Wheel variants are a clear step in the right direction in thisregard.
—Michael Hudgins,JAX Developer Infrastructure Lead
They affect everyone from package authors to end users of all skilllevels, including students, scientists and engineers:
Accessing compute to run models and process large datasets has beena pain point in scientific computing for over a decade. Today,researchers and data scientists still spend hours to days installingcore tools like PyTorch before they can begin their work. Thiscomplexity is a significant barrier to entry for users who want touse Python in their daily work. The WheelNext Wheel Variantsproposal offers a pathway to address persistent installation andcompute-access problems within the broader packaging ecosystemwithout creating another, new and separate solution. Let’s focus onthe big picture of enhancing user experience - it will make a realdifference.
—Leah Wasser, Executive Director and Founder ofpyOpenSci
Research institutions and cloud providers manage heterogeneouscomputing clusters with different architectures (CPU, Hardwareaccelerators, ASICS, etc.). The current system requiresenvironment-specific installation procedures, making reproducibledeployment difficult. This situation also contributes to making“scientific papers” difficult to reproduce. Application authors focusedon improving that are hindered by the packaging hurdles too:
We’ve been developing a package manager for Spyder, a Python IDE forscientists, engineers and data analysts, with three main aims.First, to make our users’ life easier by allowing them to createenvironments and install packages using a GUI instead of introducingarcane commands in a terminal. Second, to make their research codereproducible, so they can share it and its dependencies with theirpeers. And third, to allow users to transfer their code to machinesin HPC clusters or the cloud with no hassle, so they can leveragethe vast compute resources available there. With the improvementsproposed by this PEP, we’d be able to make that a reality for allPyPI users because installing widely used scientific libraries (likePyTorch and CuPy) for the right GPU and instruction set and would bestraightforward and transparent for tools built on top of uv/pip.
The recent advances in modern AI workflows increasingly rely on GPUacceleration, but the current packaging system makes deployment complexand adds a significant burden on open source developers of the entiretool stack (from build backends to installers, not forgetting thepackage maintainers).
PyTorch’s extensive wheel support was always state of the art andprovided hardware accelerator support from day zero via ourpackageselector. We believethis was always a superpower of PyTorch to get things working out ofthe box for our users. Unfortunately, the infrastructure supportingthese is very complex, hard to maintain and inefficient (for us, ourusers and package repositories).
With the number of hardware we support growing rapidly again, we arevery supportive of the wheel variants efforts that will allow us toget PyTorch install instructions to be what our users have beenexpecting since PyTorch was first released:pipinstalltorch
The lead maintainer ofXGBoost enumerates anumber of problems XGBoost has that he expects will be addressed bywheel variants:
Large download size, due to the use of “fat binaries” for multipleSMs [GPU targets]. Currently, XGBoost builds for 11 different SMs.
The need for a separate packaging name for CPU-only package.Currently we ship a separate package namedxgboost-cpu,requiring users to maintain separaterequirements.txt files.Seexgboost#11632 for an example.
Complex dispatching logic for multiple CUDA versions. Somefeatures of XGBoost require new CUDA versions (12.5 or 12.8),while the XGBoost wheel targets 12.0. As a result, we maintain afairly complex dispatching logic to detect CUDA and driverversions at runtime. Such dispatching logic should be bestimplemented in a dedicated piece of software like the NVIDIAprovider plugin, so that the XGBoost project can focus on its coremission.
Undefined behavior due to presence of multiple OpenMP runtimes.XGBoost is installed in a variety of systems with different OpenMPruntimes (or none at all). So far, XGBoost has been vendoring acopy of OpenMP runtime, but this is increasingly untenable. Usersget undefined behavior such as crashes or hangs when multipleincompatible versions of OpenMP runtimes are present in thesystem. (This problem was particularly bad on MacOS, so much sothat the MacOS wheel for XGBoost no longer bundles OpenMP.)
The complexity of packaging is distracting developers from focusing onthe actual goals for their software:
We maintain a scientific software tool that uses deep learning foranalyzing biological motion in image sequences that has gottentraction (>35k users, >80 countries) due to its user friendliness asa frontend for training custom models on specialized scientificdata. Our userbase are scientists who spend all day doing brainsurgeries and molecular genetics to discover cures to diseases. Itis entirely unreasonable to expect that they should have to learnabout hardware accelerator driver compatibility matrices,environment managers, and keep up with the ever changing Pythonpackaging ecosystem just to be able to analyze their data.
In recognition of this, my team has spent an inordinate amount oftime on maintaining dependencies and packaging hacks to ensure thatour tool, which now undergirds the reproducibility of millions ofdollars worth of research studies, remains compatible with everyplatform. In the past couple of years, we estimate that we’ve spenthundreds of hours and over $250,000 of taxpayer-supported researchfunding engineering solutions to this problem. WheelNext would havesolved this entirely, allowing us to focus our efforts onunderstanding and treating neurodegenerative diseases.
—Talmo Pereira, Ph.D., author ofSLEAP and Principal Investigatorat the Salk Institute for Biological Studies
The potential for improvement can be summarized as:
This PEP is a significant step forward in improving the deploymentchallenges of the Python ecosystem in the face of increasinglycomplex and varied hardware configurations. By enabling multipledeployment targets for the same libraries in a standard way, it willconsolidate and simplify many awkward and time-consumingwork-arounds developers have been pursuing to support the rapidlygrowing AI/ML and scientific computing worlds.
—Travis Oliphant, the author ofNumPy andSciPy and Chief AIArchitect 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:
The format of a static file to select variants deterministically orinclude variants in apylock.toml file,
The list of variant providers that are vendored or re-implemented byinstallers,
The specific opt-in mechanisms and UX for allowing an installer to runnon-vendored variant providers,
How to instruct build backends to emit variants through thePEP 517mechanism.
This problem is not unique to the Python ecosystem, different groups andecosystems have come up with various answers to that very problem. Thissection will focus on highlighting the strengths and weaknesses of thedifferent approaches taken by various communities.
Conda is a binary-only package ecosystemthat uses aggregated metadata indexes for resolution rather thanfilename parsing. Unlike theSimple repository API, conda’sresolution relies onrepodata indexes per platformcontaining full metadata, making filenames purely identifiers with noparsing requirements.
Variant System: In2016-2017,conda-build introduced variants to differentiate packages with identicalname/version but different dependencies.
pytorch-2.8.0-cpu_mkl_py313_he1d8d61_100.conda# CPU + MKL variantpytorch-2.8.0-cuda128_mkl_py313_hf206996_300.conda# CUDA 12.8 + MKL variantpytorch-2.8.0-cuda129_mkl_py313_he100a2c_300.conda# CUDA 12.9 + MKL variant
A hash (computed from variant metadata) prevents filename collisions;actual variant selection happens via standard dependency constraints inthe solver. No special metadata parsing is needed—installers simplyresolve dependencies like:
condainstallpytorchmkl
Mutex Metapackages: Python metadata and conda metadata do not havegood 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. Mutexmetapackages are sets of packages with the same name, but differentbuild string. Packages depend on specific mutex builds (e.g.,blas=*=openblas vsblas=*=mkl) to avoid problems with relatedpackages using different dependency libraries, such asNumPy usingOpenBLAS andSciPy usingMKL.
Virtual Packages:Introduced in 2019, virtual packages injectsystem detection (CUDA version, glibc, CPU features) as solverconstraints. Built packages express dependencies like__cuda>=12.8,and the installer verifies compatibility at install time. Currentvirtual packages includearchspec (CPU capabilities), OS/systemlibraries, and CUDA driver version. Detection logic is tool-specific(rattler,mamba).
archspec is a library fordetecting, labeling, and reasoning about CPU microarchitecture variants,developed for theSpack package manager.
Variant Model: CPU Microarchitectures (e.g.,haswell,skylake,zen2,armv8.1a) form aDirected Acyclic Graph(DAG) encoding binary compatibility,which helps at resolve to express thatpackageB depends onpackageA. The ordering is partial because (1) separate ISA familiesare incomparable, and (2) contemporary designs may have incompatiblefeature sets—cascadelake and cannonlake are incomparable despite bothdescending from skylake, as each has unique AVX-512 extensions.
Implementation: A language-agnostic JSON database storesmicroarchitecture metadata (features, compatibility relationships,compiler-specific optimization flags). Language bindings providedetection (queries/proc/cpuinfo, matches to microarchitecture withlargest compatible feature subset) and compatibility comparisonoperators.
Package Manager Integration: Spack records target microarchitectureas package provenance (spackinstallfftwtarget=broadwell),automatically selects compiler flags, and enablesmicroarchitecture-aware binary caching. TheEuropean Environment forScientific Software Installations (EESSI)distributes optimized builds in separate subdirectories permicroarchitecture (e.g.,x86_64,armv8.1a,haswell);runtime initialization usesarchspec to select best compatible buildwhen no exact match exists.
Gentoo Linux is a source-first distributionwith support for extensive package customization. This is primarilyachieved viaUSE flags:boolean flags exposed by individual packages and permitting fine-tuningthe 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 betoggled individually, and separate binary packages can be built fordifferent sets of flags. The package manager can either pick a binarypackage with matching configuration or build from source.
API and ABI matching is primarily done through use ofslotting.Slots are generally used to provide multiple versions or variants ofgiven package that can be installed alongside (e.g. different major GTK+or LLVM versions, or GTK+3 and GTK4 builds of WebKitGTK), whereassubslots are used to group versions within a slot, usually correspondingto the library ABI version. Packages can then declare dependencies boundto the slot and subslot used at build time. Again, separate binarypackages can be built against different dependency slots. Wheninstalling a dependency version falling into a different slot orsubslot, the package manager may either replace the package needing thatdependency with a binary packages built against the new slot, or rebuildit from source.
Normally, the use of slots assumes that upgrading to the newest versionpossible is desirable. When more fine-grained control is desired, slotsare used in conjunction with USE flags. For example,llvm_slot_{major} flags are used to select a LLVM major version tobuild against.
Wheels that share the same distribution name, version, build number,and platform compatibility tags, but are distinctly identified by anarbitrary set of variant properties.
Variant Namespace
An identifier used to group related features provided by a singleprovider (e.g.,nvidia,x86_64,arm, etc.).
Variant Feature
A specific characteristic (key) within a namespace (e.g.,version,avx512_bf16, etc.) that can have one or morevalues.
Variant Property
A 3-tuple (namespace::feature-name::feature-value)describing a single specific feature and its value. If a feature hasmultiple values, each is represented by a separate property.
Variant Label
A string (up to 16 characters) added to the wheel filename touniquely identify variants.
Null Variant
A special variant with zero variant properties and the reservedlabelnull. Always considered supported but has the lowestpriority among wheel variants, while being preferably chosen overnon-variant wheels.
Variant Provider
A provider of supported and valid variant properties for a specificnamespace, usually in the form of a Python package that implementssystem detection.
Install-time Provider
A provider implemented as a plugin that can be queried during wheelinstallation.
Ahead-of-Time Provider
A provider that features a static list of supported properties whichis then embedded in the wheel metadata. Such a list can either beembedded inpyproject.toml or provided by a plugin queried atbuild time.
Wheel variants introduce a more fine-grained specification of builtwheel characteristics beyond what existing wheel tags provide.Individual wheels carry a human-readable label defined at build time, asdescribed inmodified wheel filename, and are characterizing usingvariant property system. The properties are organized into ahierarchical structure of namespaces, features and feature values. Whenevaluating wheels to install, the installer determines whether variantproperties of a given wheel are compatible with the system, and performvariant ordering based on the priority of the compatible variantproperties. This is done in addition to determining the compatibility.The ordering by variant properties takes precedence over ordering bytags.
Every variant namespace is governed by a variant provider. There are twokinds of variant providers: install-time providers and ahead-of-time(AoT) providers. Install-time providers require plugins that are queriedwhile installing wheels to determine the set of supported properties andtheir preference order. For AoT providers, this data is static andembedded in the wheel; it can be either provided directly by thewheel maintainer or queried at wheel build time from an AoT plugin.
Both kinds of plugins are usually implemented as Python packages whichimplement theprovider plugin API, but they may also be vendored orreimplemented by installers to improve security, as outlined inProviders. Plugin packages may be installed in isolated ornon-isolated environments. In particular, all plugins may be returned bytheget_requires_for_build_wheel() hook of aPEP 517 backend, andtherefore installed along with other build dependencies. For thisreason, it is important that plugin packages do not narrowly pindependencies, as that could prevent different packages from beinginstalled simultaneously in the same environment.
Metadata governing variant support is defined inpyproject.tomlfile, and it is copied intovariant.json file in wheels, as exploredinmetadata in source tree and wheels. Additionally,variantenvironment markers can be used to define dependencies specific to asubset of variants.
One of the core requirements of the design is to ensure that installerspredating this PEP will ignore wheel variant files. This makes itpossible to publish both variant wheels and non-variant wheels on asingle index, with installers that do not support variants securelyignoring the former, and falling back to the latter.
A variant label component is added to the filename for the twofoldpurpose of providing a unique mapping from the filename to a set ofvariant properties, and providing a human-readable identification forthe variant. The label is kept short and lowercase to avoid issues withdifferent filesystems. It is added as a--separated component at theend to ensure that the existing filename validation algorithms rejectit:
If both the build tag and the variant label are present, the filenamecontains too many components. Example:
If only the variant label is present, the Python tag at third positionwill be misinterpreted as a build number. Since the build number muststart with a digit and no Python tags at the time start with digits,the filename is considered invalid. Example:
Variant properties serve the purpose of expressing the characteristicsof the variant. Unlike platform compatibility tags, they are stored inthe variant metadata and therefore do not affect the wheel filenamelength. They follow a hierarchical key-value design, with the keyfurther broken into a namespace and a feature name. Namespaces are usedto group features defined by a single provider, and to avoid conflictsshould multiple providers define a feature with the same name. Thispermits 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 thesame name. The character set for values is more relaxed, to permitvalues resembling versions.
Variant properties are serialized into a structured 3-tuple formatinspired by Trove Classifiers inPEP 301:
{namespace} :: {feature_name} :: {feature_value}
Properties are used both to determine variant wheel compatibility, andto select the best variant to install. Provider plugins indicate whichvariant properties are compatible with the system, and order them byimportance. This ordering can further be altered in variant wheelmetadata.
Variant features can be declared as allowing multiple values to bepresent within a single variant wheel. If that is the case, these valuesare matched as a logical OR, i.e. only a single value needs tobe 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 themneed to be compatible. This provides some flexibility in designatingvariant compatibility while avoiding having to implement a completeboolean logic.
Typically, variant features will be single-value and indicate minimal ormutually exclusive requirements. The system may indicate multiplecompatible values. For example, if the feature declares a minimum CUDAruntime version, the provider will indicate compatibility with wheelsrequiring a minimum version corresponding to the currently installedversion or older, e.g. for CUDA 12.8, the compatible minimum versionsused in wheels would be, in order of decreasing preference:
Similarly, a wheel could indicate its minimum required CPU version, andthe provider will indicate all the compatible CPU versions.
Multi-value features are useful for “fat” packages where multipleincompatible targets are supported by a single package. A typicalexample are GPUs. In this case, the wheel declares a number of supportedGPUs, and the provider indicates which GPUs are actually installed(usually one). The wheel is compatible if there is overlap between thetwo lists.
A null variant is a variant wheel with no properties, but distinctfrom non-variant wheels in having thenull variant label and variantmetadata. During the transition period, it provides the possibility ofproviding a distinct fallback for systems that do not support any ofthe variants provided, and for systems that do support variant wheels atall.
For example, a package with optional GPU support could publish threekinds of wheels:
Multiple GPU-enabled wheels, each built for a single CUDA version witha matching set of supported GPUs, and used only when the providerplugin indicates that the system is compatible.
A CPU-only null variant, much smaller than the GPU variants, installedwhen the provider plugin indicates that no compatible GPU isinstalled.
A GPU+CPU non-variant wheel, that will be installed on systems withoutan installer supporting variants.
Publishing a null variant is optional, and makes sense only if distinctfallbacks provide advantages to the user. If one is published, a wheelvariant-enabled installer will prefer it over the non-variant wheel. Ifit is not, it will fall back to the non-variant wheel instead. Thenon-variant wheel is also used if variant support is explicitly disabledby an installer flag.
The null variant uses a reservednull label to make it clearlydistinguishable from regular variants.
The variant wheel metadata specifies what providers are used for itsproperties. Providers serve a twofold purpose:
at install time: determining which variant wheels are compatible withthe user’s system, and which of them constitutes the best choice, and
at build time: determining which variant properties are valid forbuilding a wheel.
The specification proposes two kinds of providers: install-timeproviders and Ahead-of-Time providers.
Install-time providers are implemented either as Python packages thatneed to be installed and run to query them, or vendored or reimplementedin the tools. They are used when user systems need to be queried todetermine wheel compatibility, for example for variants utilizing GPUsor requiring CPU instruction sets beyond what platform tags provide.Installing third-party packages involves security risks highlighted inthesecurity implications section, and the proposed mitigations incura cost on installer implementations.
Ahead-of-Time providers are implemented as static metadata embedded inthe wheel. They are used when particular variant properties are alwayscompatible with the user’s system (provided that a wheel using them hasbeen built successfully). However, the metadata indicates whichproperties are preferred. For example, AoT providers can be used toprovide choice between builds against different BLAS / LAPACK providers,or to provide debug builds of packages. Since they do not requirerunning code external to the installer, they do not pose the problemsfaced by install-time providers, and can be used more liberally.
AoT providers are permitted to feature plugin packages. If that is thecase, these packages are only used when building wheels, and theiroutput is used to fill in the static metadata used at install time.This way, it is easier to use consistent property names and valuesacross multiple packages. Otherwise, the package maintainer needs toinclude the supported properties directly in thepyproject.tomlfile.
When implemented as Python packages, both kinds of provider pluginsexpose roughly the same API. However, an AoT provider must alwaysconsider all valid variant properties supported, and it must alwaysreturn the same ordered list of supported properties irrespective of theuser system. All AoT providers can technically be used as install-timeproviders, but not the other way around.
As the specification introduces the potential necessity of installingand running provider packages to install wheels, it is recommended thatthese packages remain functioning correctly for the variant wheelspublished in the past, including very old package versions. Ideally, noproperties previously supported should ever be removed.
If a breaking change needs to be performed, it is recommended to eitherintroduce a new provider package for that, or add a new plugin APIendpoint to the existing package. In both cases, it may be necessary topreserve the old endpoint in minimal maintenance mode, to ensure thatold wheels can still be installed. The old endpoint can triggerdeprecation warnings in theget_all_configs() hook that is used whenbuilding packages.
An alternative approach is to use semantic versioning to cut offbreaking changes. However, this relies on package authors reliably usingcaps on dependencies, as otherwise old wheels will start usingincompatible plugin versions. This is already a problem with Pythonbuild backends used today.
When vendoring or reimplementing plugins, installers need to followtheir current behavior. In particular, they should recognize therelevant provider versions numbers, and possibly fall back to installingthe external plugin when the package in question is incompatible withthe installer’s implementation.
Variants introduce a few new portions of metadata that are stored in thesource tree and in wheels. In the source tree, it is stored in thepyproject.toml file along with other project properties, benefitingfrom the TOML format’s readability and strictness. Afterwards, it isconverted into an equivalent JSON structure, and stored as a separatefile in the.dist-info directory. The existing metadata files areunchanged to avoid unnecessary incompatibility, and to avoid serializinginto the inconvenientCore Metadata format.
The metadata inpyproject.toml includes:
information about variant providers that could be used by the wheels,
optionally, lists overriding the default property ordering,
static property lists for Ahead-of-Time providers that do not useplugins.
In wheel metadata, the above is amended by static property listsobtained from the plugins and variant properties for the built wheel.
When wheels are published on an index, the variant metadata from allwheels is combined into a single{name}-{version}-variants.json filethat is used by clients to efficiently obtain the variant metadatawithout having to download it from individual wheels separately, orimplement explicit variant metadata support in an API provided by thepackage index server.
Some packages provide extension modules exposing an Application BinaryInterface (ABI) that is not compatible across wide ranges of versions.The packages using this interface need to pin their wheels to theversion used at build time. If ABI changes frequently, the pins are verynarrow and users face problems if they need to install two packages thatmay happen to pin to different versions of the same dependency.Providing variants built against different dependency versions canincrease the chance of a resolver being able to find a dependency versionthat is compatible with all the packages being installed.
Unfortunately, such a variant provider cannot be implemented within theplugin API defined by the specification. Given that a robustimplementation would need to interface with the dependency resolver,rather than attempt to extend the API to cover this use case and addsignificant complexity as a result, the specification reservesabi_dependency as a special variant namespace that can beimplemented by installers wishing the provide this feature.
Given the complexity of the problem, this extension is made entirelyoptional. This implies that any packages using it need to providenon-variant wheels as well.
As of October 2025,PyTorch publishes a total of sevenvariants for every release: a CPU-only variant, three CUDA variants withdifferent minimal CUDA runtime versions and supported GPUs, two ROCmvariants and a Linux XPU variant.
This setup could be improved using GPU/XPU plugins that query theinstalled runtime version and installed GPUs/XPUs to filter out thewheels for which the runtime is unavailable, it is too old or the user’sGPU is not supported, and order the remaining variants by the runtimeversion. The CPU-only version is published as a null variant that isalways supported.
If a GPU runtime is available and supported, the installer automaticallychooses the wheel for the newest runtime supported. Otherwise, it fallsback to the CPU-only variant. In the corner case when multipleaccelerators are available and supported, PyTorch package maintainersindicate which one takes preference by default.
Wheel variants can be used to provide variants requiring specific CPUextensions, beyond what platform tags currently provide. They can beparticularly helpful when runtime dispatching is impractical, when thepackage relies on prebuilt components that use instructions above thebaseline, when availability of instruction sets implies library ABIchanges, or simply to benefit from compiler optimizations such asauto-vectorization applied across the code base.
For example, an x86-64 CPU plugin can detect the capabilities for theinstalled CPU, mapping them onto the appropriate x86-64 architecturelevel and a set of extended instruction sets. Variant wheels indicatewhich level and/or instruction sets are required. The installer filtersout variants that do not meet the requirements and select the bestoptimized variant. A non-variant wheel can be used to represent thearchitecture baseline, if supported.
Implementation using wheel variants makes it possible to providefine-grained indication of instruction sets required, with plugins thatcan be updated as frequently as necessary. In particular, it is neithernecessary to cover all available instruction sets from the start, nor toupdate the installers whenever the instruction set coverage needs to beimproved.
Packages such asNumPy andSciPy can be built using different BLAS /LAPACK libraries. Users may wish to choose a specific library forimproved performance on a particular hardware, or based on licenseconsiderations. Furthermore, different libraries may use differentOpenMP implementations, whereas using a consistent implementation acrossthe stack can avoid degrading performance through spawning too manythreads.
BLAS / LAPACK variants do not require a plugin at install time, sinceall variants built for a particular platform are compatible with it.Therefore, an ahead-of-time provider (withinstall-time=false)that provides a predefined set of BLAS / LAPACK library names can beused. When the package is installed, normally the default variant isused, but the user can explicitly select another one.
A package may wish to provide a special debug-enabled builds fordebugging or CI purposes, in addition to the regular release build. Forthis purpose, an optional ahead-of-time provider can be used(install-time=false withoptional=true), defining a customproperty for the debug builds. Since the provider is disabled bydefault, users normally install the non-variant wheel providing therelease build. However, they can easily obtain the debug build byenabling the optional provider or selecting the variant explicitly.
Packages such asvLLMneed to be pinned to the PyTorch version they were built against topreserve Application Binary Interface (ABI) compatibility. This oftenresults in unnecessarily strict pins in package versions, making itimpossible to find a satisfactory resolution for an environmentinvolving multiple packages requiring different versions of PyTorch, orresorting to source builds. Variant wheels can be used to publishvariants of vLLM built against different PyTorch versions, thereforeenabling upstream to easily provide support for multiple versionssimultaneously.
The optionalabi_dependency extension can be used to build multiplevllm variants that are pinned to different PyTorch versions, e.g.:
The proposal introduces a plugin system for querying the systemcapabilities in order to determine variant wheel capability. The systempermits specifying additional Python packages providing the pluginsin the package index metadata. Installers and other tools that needto determine whether a particular wheel is installable, or selectthe most preferred variant among multiple variant wheels, may needto install these packages and execute the code within them whileresolving dependencies or processing wheels.
This elevates the supply-chain attack potential by introducing two newpoints for malicious actors to inject arbitrary code payload:
Publishing a version of a variant provider plugin or one of itsdependencies with malicious code.
Introducing a malicious variant provider plugin in an existingpackage metadata.
While such attacks are already possible at the package dependency level,it needs to be emphasized that in some scenarios the affected tools areexecuted with elevated privileges, e.g. when installing packages formulti-user systems, while the installed packages are only used withregular user privileges afterwards. Therefore, variant provider pluginscould introduce a Remote Code Execution vulnerability with elevatedprivileges.
A similar issue already exists in the packaging ecosystem when packagesare installed from source distributions, whereas build backendsand other build dependencies are installed and executed. However,various tools operating purely on wheels, as well as users usingtool-specific options to disable use of source distributions,have been relying on the assumption that no code external to the systemwill be executed while resolving dependencies, installing a wheel orotherwise processing it. To uphold this assumption, the proposalexplicitly requires that untrusted provider plugin packages are never installedwithout explicit user consent.
TheProviders section of the specification provides furthersuggestions that aim to improve both security and the user experience.It is expected that a limited subset of popular provider plugins willbe either vendored by the installer, eliminating the use of packagesexternal to the tool altogether, or pinned to specific versions,providing the same level of code auditing as the tools themselves.This will lead to the majority of packages focusing on these specificplugins. External plugins requiring explicit opt-in should be rare,minimizing the workflow disruption and reducing the risk that usersblanket-allow all plugins.
Furthermore, the specification permits using static configuration asinput to skip running plugins altogether.
This PEP proposes a set of extensions to theBinary distribution format specification that enablebuilding additional variants of wheels that can be installed byvariant-aware tools while being ignored by programs that do notimplement this specification.
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”,“SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in thisdocument are to be interpreted as described inRFC 2119.
Wheels using extensions introduced by this PEP MUST feature the variantlabel component. The label MUST adhere to the following rules:
Lower case only (to prevent issues with case-sensitivevs. case-insensitive filesystems)
Between 1-16 characters
Using only0-9,a-z,. or_ ASCII characters
This is equivalent to the following regular expression:^[0-9a-z._]{1,16}$.
Every label MUST uniquely correspond to a specific set of variantproperties, which MUST be the same for all wheels using the same label within a singlepackage version. Variant labels SHOULD be specified at wheel build time,as human-readable strings. The labelnull is reserved for the nullvariant and MUST use an empty set of variant properties.
Installers that do not implement this specification MUST ignore wheelswith variant label when installing from an index, and fall back to awheel without such label if it is available. If no such wheel isavailable, the installer SHOULD output an appropriate diagnostic,in particular warning if it results in selecting an earlier packageversion or a clear error if no package version can be installed.
Every variant wheel MUST be described by zero or more variantproperties. A variant wheel with exactly zero properties represents thenull variant. The properties are specified when the variant wheel isbeing built, using a mechanism defined by the project’s build backend.
Each variant property is described by a 3-tuple that is serialized intothe following format:
{namespace}::{feature_name}::{feature_value}
The namespace MUST consist only of0-9,a-z and_ ASCIIcharacters (^[a-z0-9_]+$). It MUST correspond to a single variantprovider.
The feature name MUST consist only of0-9,a-z and_ ASCIIcharacters (^[a-z0-9_]+$). It MUST correspond to a valid featurename defined by the respective variant provider in the namespace.
The feature value MUST consist only of0-9,a-z,_ and.ASCII Characters (^[a-z0-9_.]+$). It MUST correspond to a validvalue defined by the respective variant provider for the feature.
If a feature is marked as “multi-value” by the provider plugin, a singlevariant wheel can define multiple properties sharing the same namespaceand feature name. Otherwise, there MUST NOT be more than a single valuecorresponding to a single pair of namespace and feature name within avariant wheel.
For a variant wheel to be considered compatible with the system, all ofthe features defined within it MUST be determined to be compatible. Fora feature to be compatible, at least a single value corresponding to itMUST be compatible.
Examples:
# all of the following must be supportedx86_64 :: level :: v3x86_64 :: avx512_bf16 :: onnvidia :: cuda_version_lower_bound :: 12.8# additionally, at least one of the following must be supportednvidia :: sm_arch :: 120_realnvidia :: sm_arch :: 110_real
When installing or resolving variant wheels, installers SHOULD query thevariant provider to verify whether a given wheel’s properties arecompatible with the system and to select the best variant throughvariant ordering. However, they MAY provide an option to omit theverification and install a specified variant explicitly.
Providers can be marked as install-time or ahead-of-time. Forinstall-time providers, installers MUST use the provider package or anequivalent reimplementation to query variant property compatibility. Forahead-of-time providers, they MUST use the static metadata embedded inthe 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 insteadassume that its properties are incompatible. It SHOULD provide an optionto enable optional providers.
Providers can also be made conditional toEnvironment Markers. If that is the case,the installer MUST check the markers against the environment to whichwheels are going to be installed. It MUST NOT use any providers whosemarkers do not match, and instead assume that their properties areincompatible.
All the tools that need to query variant providers and are run in asecurity-sensitive context, MUST NOT install or run code from anyuntrusted package for variant resolution without explicit user opt-in.Install-time provider packages SHOULD take measures to guard againstsupply chain attacks, for example by vendoring all dependencies.
It is RECOMMENDED that said tools vendor, reimplement or lock the mostcommonly used plugins to specific wheels. For plugins and theirdependencies that are neither reimplemented, vendored nor otherwisevetted, a trust-on-first-use mechanism for every version is RECOMMENDED.In interactive sessions, the tool can explicitly ask the user forapproval. In non-interactive sessions, the approval can be given usingcommand-line interface options. It is important that the user isinformed of the risk before giving such an approval.
For a consistent experience between tools, variant wheels SHOULD besupported by default. Tools MAY provide an option to only usenon-variant wheels.
This section describes the metadata format for the providers, variantsand properties of a package and its wheels. The format is used in threelocations, with slight variations:
in the source tree, inside thepyproject.toml file
in the built wheel, as a*.dist-info/variant.json file
on the package index, as a{name}-{version}-variants.json file.
All three variants metadata files share a common JSON-compatiblestructure:
The top-level object is a dictionary rooted at a specific point in thecontaining file. Its individual keys are sub-dictionaries that aredescribed in the subsequent sections, along with the requirements fortheir presence. The tools MUST ignore unknown keys in the dictionariesfor forwards compatibility of updates to the PEP. However, usersMUST NOT use unsupported keys to avoid potential future conflicts.
AJSON schema is included in the Appendixof this PEP, to ease comprehension and validation of the metadataformat. This schema will be updated with each revision to the variantmetadata specification. The schema is available inAppendix: JSON Schema for Variant Metadata.
Ultimately, the variant metadata JSON schema SHOULD be served bypackaging.python.org.
providers is a dictionary, the keys are namespaces, the values aredictionaries with provider information. It specifies how to install anduse variant providers. A provider information dictionary MUST bedeclared inpyproject.toml for every variant namespace supported bythe package. It MUST be copied tovariant.json as-is, includingthe data for providers that are not used in the particular wheel.
A provider information dictionary MAY contain the following keys:
enable-if:str: Anenvironment marker defining when the pluginshould be used.
install-time:bool: Whether this is an install-time provider.Defaults totrue.false means that it is an AoT providerinstead.
optional:bool: Whether the provider is optional. Defaultstofalse. If it istrue, the provider isconsidered optional.
plugin-api:str: The API endpoint for the plugin. If it isspecified, it MUST be an object reference as explained in theAPIendpoint section. If it is missing, the package name from the firstdependency specifier inrequires is used, after replacing all- characters with_ in the normalized package name.
requires:list[str]: A list of zero or more packagedependency specifiers, that are used toinstall the provider plugin. If the dependency specifiers includeenvironment markers, these are evaluated against the environment wherethe plugin is being installed and the requirements for which themarkers evaluate to false are filtered out. In that case, at leastone dependency MUST remain present in every possible environment.Additionally, ifplugin-api is not specified, the first dependencypresent after filtering MUST always evaluate to the same API endpoint.
All the fields are OPTIONAL, with the following exceptions:
Ifinstall-time is true, the dictionary describes an install-timeprovider and therequires key MUST be present and specify atleast one dependency.
Ifinstall-time is false, it describes an AoT provider and therequires key is OPTIONAL. In that case:
Ifrequires is provided and non-empty, the provider dictionaryMUST reference an AoT provider plugin that will be queried atbuild time to fillstatic-properties.
Otherwise,static-properties MUST be specified inpyproject.toml.
Thedefault-priorities dictionary controls the ordering of variants.The exact algorithm is described in theVariant ordering section.
It has a single REQUIRED key:
namespace:list[str]: All namespaces used by the wheel variants,ordered in decreasing priority. This list MUST have the same membersas the keys of theproviders dictionary.
It MAY have the following OPTIONAL keys:
feature:dict[str,list[str]]: A dictionary with namespaces askeys, and ordered list of corresponding feature names as values. Thevalues in each list override the default ordering from the provideroutput. They are listed from the highest priority to the lowestpriority. Features not present on the list are considered of lowerpriority than those present, and their relative priority is defined bythe plugin.
property:dict[str,dict[str,list[str]]]: A nested dictionarywith namespaces as first-level keys, feature names as second-levelkeys and ordered lists of corresponding property values assecond-level values. The values present in the list override thedefault ordering from the provider output. They are listed from thethe highest priority to the lowest priority. Properties not present onthe list are considered of lower priority than these present, andtheir relative priority is defined by the plugin output.
Thestatic-properties dictionary specifies the supported propertiesfor AoT providers. It is a nested dictionary with namespaces as firstlevel keys, feature name as second level keys and ordered lists offeature values as second level values.
Inpyproject.toml file, the namespaces present in this dictionaryMUST correspond to all AoT providers without aplugin (i.e. withinstall-time offalse and no or emptyrequires). When building a wheel, the build backend MUST query theAoT provider plugins (i.e. these withinstall-time beingfalseand non-emptyrequires) to obtain supported properties and embedthem into the dictionary. Therefore, the dictionary invariant.jsonand*-variants.json MUST contain namespaces for all AoT providers(i.e. all providers withinstall-time beingfalse).
Since TOML and JSON dictionaries are unsorted, so are the features inthestatic-properties dictionary. If more than one feature isspecified for a namespace, then the order for all features MUST bespecified indefault-priorities.feature.{namespace}. If an AoTplugin is used to fillstatic-properties, then the features notalready in the list inpyproject.toml MUST be appended to it.
The list of values is ordered from the most preferred to the leastpreferred, same as the lists returned byget_supported_configs()plugin API call (as defined inplugin interface). Thedefault-priorities.property dict can be used to override theproperty ordering.
Thevariants dictionary is used invariant.json to indicate thevariant that the wheel was built for, and in*-variants.json toindicate all the wheel variants available. It’s a 3-level dictionarylisting all properties per variant label: The first level keys arevariant labels, the second level keys are namespaces, the third levelare feature names, and the third level values are lists of featurevalues.
Thepyproject.toml file is the standard project configuration fileas defined inpyproject.toml specification. Thevariant metadata MUST be rooted at a top-level table namedvariant.It MUST NOT specify thevariants dictionary. It is used by buildbackends to build variant wheels.
Example Structure:
[variant.default-priorities]# prefer CPU features over BLAS/LAPACK variantsnamespace=["x86_64","aarch64","blas_lapack"]# prefer aarch64 version and x86_64 level features over other features# (specific CPU extensions like "sse4.1")feature.aarch64=["version"]feature.x86_64=["level"]# prefer x86-64-v3 and then older (even if CPU is newer)property.x86_64.level=["v3","v2","v1"][variant.providers.aarch64]# example using different package based on Python versionrequires=["provider-variant-aarch64 >=0.0.1; python_version >= '3.12'","legacy-provider-variant-aarch64 >=0.0.1; python_version < '3.12'",]# use only on aarch64/arm machinesenable-if="platform_machine == 'aarch64' or 'arm' in platform_machine"plugin-api="provider_variant_aarch64.plugin:AArch64Plugin"[variant.providers.x86_64]requires=["provider-variant-x86-64 >=0.0.1"]# use only on x86_64 machinesenable-if="platform_machine == 'x86_64' or platform_machine == 'AMD64'"plugin-api="provider_variant_x86_64.plugin:X8664Plugin"[variant.providers.blas_lapack]# plugin-api inferred from requiresrequires=["blas-lapack-variant-provider"]# plugin used only when building package, properties will be inlined# into variant.jsoninstall-time=false
Thevariant.json file MUST be present in the*.dist-info/directory of a built variant wheel. It is serialized into JSON, with thevariant metadata dictionary being the top object. It MUST include allthe variant metadata present inpyproject.toml, copied as indicatedin the individual key sections. In addition to that, it MUST contain:
a$schema key whose value is the URL corresponding to the schemafile supplied in the appendix of this PEP. The URL contains theversion of the format, and a new version MUST be added to the appendixwhenever the format changes in the future,
avariants object listing exactly one variant - the variantprovided by the wheel.
The variant.json file corresponding to the wheel built from the examplepyproject.toml file for x86-64-v3 would look like:
{// The schema URL will be replaced with the final URL on packaging.python.org"$schema":"https://variants-schema.wheelnext.dev/v0.0.3.json","default-priorities":{"feature":{"aarch64":["version"],"x86_64":["level"]},"namespace":["x86_64","aarch64","blas_lapack"],"property":{"x86_64":{"level":["v3","v2","v1"]}}},"providers":{"aarch64":{"enable-if":"platform_machine == 'aarch64' or 'arm' in platform_machine","plugin-api":"provider_variant_aarch64.plugin:AArch64Plugin","requires":["provider-variant-aarch64 >=0.0.1; python_version >= '3.12'","legacy-provider-variant-aarch64 >=0.0.1; python_version < '3.12'"]},"blas_lapack":{"install-time":false,"requires":["blas-lapack-variant-provider"]},"x86_64":{"enable-if":"platform_machine == 'x86_64' or platform_machine == 'AMD64'","plugin-api":"provider_variant_x86_64.plugin:X8664Plugin","requires":["provider-variant-x86-64 >=0.0.1"]}},"static-properties":{"blas_lapack":{"provider":["accelerate","openblas","mkl"]},},"variants":{// always a single entry, expressing the variant properties of the wheel"x8664v3_openblas":{"blas_lapack":{"provider":["openblas"]},"x86_64":{"level":["v3"]}}}}
For every package version that includes at least one variant wheel,there MUST exist a corresponding{name}-{version}-variants.jsonfile, 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 theFile name convention of the Binary Distribution Formatspecification. The link to this file MUST be present on all index pageswhere the variant wheels are linked. It is presented in the same simplerepository format as source distribution and wheel links in the index,including an (OPTIONAL) hash.
This file uses the same structure asvariant.json described above,except that the variants object MUST list all variants available on thepackage index for the package version in question. It is RECOMMENDEDthat tools enforce the same contents of thedefault-priorities,providers andstatic-properties sections for all variants listedin the file, though careful merging is possible, as long as noconflicting information is introduced, and the resolution results withina subset of variants do not change.
Thefoo-1.2.3-variants.json corresponding to the package with twowheel variants, one of them listed in the previous example, would looklike:
{// The schema URL will be replaced with the final URL on packaging.python.org"$schema":"https://variants-schema.wheelnext.dev/v0.0.3.json","default-priorities":{// identical to above},"providers":{// identical to above},"static-properties":{// identical to above},"variants":{// all available wheel variants"x8664v3_openblas":{"blas_lapack":{"provider":["openblas"]},"x86_64":{"level":["v3"]}},"x8664v4_mkl":{"blas_lapack":{"provider":["mkl"]},"x86_64":{"level":["v4"]}}}}
To determine which variant wheel to install when multiple wheels arecompatible, variant wheels MUST be ordered by their variant properties.
For the purpose of ordering, variant properties are grouped intofeatures, and features into namespaces. The ordering MUST be equivalentto the following algorithm:
Construct the ordered list of namespaces by copying the value of thedefault-priorities.namespace key.
For every namespace:
Construct the initial ordered list of feature names by copying thevalue of the respectivedefault-priorities.feature.{namespace}key.
Obtain the supported feature names from the provider, in order.For every feature name that is not present in the constructedlist, append it to the end.
After this step, a list of ordered feature names is available forevery namespace.
For every feature:
Construct the initial ordered list of values by copying the valueof the respectivedefault-priorities.property.{namespace}.{feature_name} key.
Obtain the supported values from the provider, in order. Forevery value that is not present in the constructed list, appendit to the end.
After this step, a list of ordered property values is available forevery feature.
For every variant property present in at least one of the compatiblevariant wheels, construct a sort key that is a 3-tuple consisting ofits namespace, feature name and feature value indices in therespective ordered lists.
For every compatible variant wheel, order its properties by theirsort keys, in ascending order.
To order variant wheels, compare their sorted properties. If theproperties at the first position are different, the variant with thelower 3-tuple of the respective property is sorted earlier. If theyare the same, compare the properties at the second position, and soon, until either a tie-breaker is found or the list of properties ofone wheel is exhausted. In the latter case, the variant with moreproperties is sorted earlier.
After this process, the variant wheels are sorted from the mostpreferred to the least preferred. The null variant naturally sorts afterall the other variants, and the non-variant wheel MUST be sorted afterthe null variant. Multiple wheels with the same variant set (andmultiple non-variant wheels) MUST then be ordered according to theirplatform compatibility tags.
Alternatively, the sort algorithm for variant wheels could be describedusing the following pseudocode. For simplicity, this code does notaccount for non-variant wheels or tags.
fromtypingimportSelfdefget_supported_feature_names(namespace:str)->list[str]:"""Get feature names from plugin's get_supported_configs()"""...defget_supported_feature_values(namespace:str,feature_name:str)->list[str]:"""Get feature values from plugin's get_supported_configs()"""...# default-priorities dict from variant metadatadefault_priorities={"namespace":[...],# : list[str]"feature":{...},# : dict[str, list[str]]"property":{...},# : dict[str, dict[str, list[str]]]}# 1. Construct the ordered list of namespaces.namespace_order=default_priorities["namespace"]feature_order={}value_order={}fornamespaceinnamespace_order:# 2. Construct the ordered lists of feature names.feature_order[namespace]=default_priorities["feature"].get(namespace,[])forfeature_nameinget_supported_feature_names(namespace):iffeature_namenotinfeature_order[namespace]:feature_order[namespace].append(feature_name)value_order[namespace]={}forfeature_nameinfeature_order[namespace]:# 3. Construct the ordered lists of feature values.value_order[namespace][feature_name]=(default_priorities["property"].get(namespace,{}).get(feature_name,[]))forfeature_valueinget_supported_feature_values(namespace,feature_name):iffeature_valuenotinvalue_order[namespace][feature_name]:value_order[namespace][feature_name].append(feature_value)defproperty_key(prop:tuple[str,str,str])->tuple[int,int,int]:"""Construct a sort key for variant property (akin to step 4.)"""namespace,feature_name,feature_value=propreturn(namespace_order.index(namespace),feature_order[namespace].index(feature_name),value_order[namespace][feature_name].index(feature_value),)classVariantWheel:"""Example class exposing properties of a variant wheel"""properties:list[tuple[str,str,str]]def__lt__(self:Self,other:Self)->bool:"""Variant comparison function for sorting (akin to step 6.)"""forself_prop,other_propinzip(self.properties,other.properties):ifself_prop!=other_prop:returnproperty_key(self_prop)<property_key(other_prop)returnlen(self.properties)>len(other.properties)# A list of variant wheels to sort.wheels:list[VariantWheel]=[...]forwheelinwheels:# 5. Order variant wheel properties by their sort keys.wheel.properties.sort(key=property_key)# 6. Order variant wheels by comparing their sorted properties# (see VariantWheel.__lt__())wheels.sort()
.._pylock-packages-variants-json:``[packages.variants-json]``-----------------------------**Type**: table-**Required?**: no; requires that:ref:`pylock-packages-wheels` is used, mutually-exclusive with:ref:`pylock-packages-vcs`,:ref:`pylock-packages-directory`, and:ref:`pylock-packages-archive`.-**Inspiration**: uv_- The URL or path to the``variants.json`` file.- Only used if the project uses:ref:`wheel variants <wheel-variants>`..._pylock-packages-variants-json-url:``packages.variants-json.url``''''''''''''''''''''''''''''''See:ref:`pylock-packages-archive-url`..._pylock-packages-variants-json-path:``packages.variants-json.path``'''''''''''''''''''''''''''''''See:ref:`pylock-packages-archive-path`..._pylock-packages-variants-json-hashes:``packages.variants-json.hashes``'''''''''''''''''''''''''''''''''See:ref:`pylock-packages-archive-hashes`.
If there is a[packages.variants-json] section, the installer SHOULDresolve variants to select the best wheel file.
Every provider plugin MUST operate within a single namespace. Thisnamespace is used as a unique key for all plugin-related operations. Allthe properties defined by the plugin are bound within the plugin’snamespace, and the plugin defines all the valid feature names and valueswithin that namespace.
Provider plugin authors SHOULD choose namespaces that can be clearlyassociated with the project they represent, and avoid namespaces thatrefer to other projects or generic terms that could lead to namingconflicts in the future.
All variants published on a single index for a specific package versionMUST use the same provider for a given namespace. Attempting to loadmore than one plugin for the same namespace in the same release versionMUST result in a fatal error. While multiple plugins for the samenamespace MAY exist across different packages or release versions (suchas when a plugin is forked due to being unmaintained), they are mutuallyexclusive within any single release version.
To make it easier to discover and install plugins, they SHOULD bepublished in the same indexes that the packages using them. Inparticular, packages published to PyPI MUST NOT rely on plugins thatneed to be installed from other indexes.
Except for namespaces reserved as part of this PEP, installable Pythonpackages MUST be provided for plugins. However, as noted in theProviders section, these plugins can also be reimplemented by toolsneeding them. In the latter case, the resulting reimplementation doesnot need to follow the API defined in this section.
A plugin implemented as Python package exposes two kinds of objects at aspecified API endpoint:
attributes that return a specific value after being accessed via:
{API endpoint}.{attribute name}
callables that are called via:
{API endpoint}.{callable name}({arguments}...)
These can be implemented either as modules, or classes with classmethods or static methods. The specifics are provided in the subsequentsections.
The location of the plugin code is called an “API endpoint”, and it isexpressed using the object reference notation following theEntry points specification:
{import_path}(:{object_path})?
An API endpoint specification is equivalent to the following Pythonpseudocode:
in theplugin-api key of variant metadata, either explicitly orinferred from the package name in therequires key. This is theprimary method of using the plugin when building and installingwheels.
as the value of an installed entry point in thevariant_pluginsgroup. The name of said entry point is insignificant. This isOPTIONAL but RECOMMENDED, as it permits variant-related utilities todiscover variant plugins installed to the user’s environment.
The variant feature config class is used as a return value in plugin APIfunctions. It defines a single variant feature, along with a list ofpossible values. Depending on the context, the order of values MAY besignificant. It is defined using the following protocol:
fromabcimportabstractmethodfromtypingimportProtocolclassVariantFeatureConfigType(Protocol):@property@abstractmethoddefname(self)->str:"""Feature name"""raiseNotImplementedError@property@abstractmethoddefmulti_value(self)->bool:"""Does this property allow multiple values per variant?"""raiseNotImplementedError@property@abstractmethoddefvalues(self)->list[str]:"""List of values, possibly ordered from most preferred to least"""raiseNotImplementedError
A “variant feature config” MUST provide the following properties orattributes:
name:str specifying the feature name.
multi_value:bool specifying whether the feature is allowed tohave multiple corresponding values within a single variant wheel. Ifit isFalse, then it is an error to specify multiple values forthe feature.
values:list[str] specifying feature values. In contexts where theorder is significant, the values MUST be ordered from the mostpreferred to the least preferred.
All features are interpreted as being within the plugin’s namespace.
The plugin interface MUST follow the following protocol:
fromabcimportabstractmethodfromtypingimportProtocolclassPluginType(Protocol):# Note: properties are used here for docstring purposes, these# must be actually implemented as attributes.@property@abstractmethoddefnamespace(self)->str:"""The provider namespace"""raiseNotImplementedError@propertydefis_aot_plugin(self)->bool:"""Is this plugin valid for `install-time = false`?"""returnFalse@classmethod@abstractmethoddefget_all_configs(cls)->list[VariantFeatureConfigType]:"""Get all valid configs for the plugin"""raiseNotImplementedError@classmethod@abstractmethoddefget_supported_configs(cls)->list[VariantFeatureConfigType]:"""Get supported configs for the current system"""raiseNotImplementedError
The plugin interface MUST define the following attributes:
namespace:str specifying the plugin’s namespace.
is_aot_plugin:bool indicating whether the plugin is a valid AoTplugin. If that is the case,get_supported_configs() MUST alwaysreturn the same value asget_all_configs() (modulo ordering),which MUST be a fixed list independent of the platform on which theplugin is running. Defaults toFalse if unspecified.
The plugin interface MUST provide the following functions:
get_all_config()->list[VariantFeatureConfigType] that returns alist of “variant feature configs” describing all valid variantfeatures within the plugin’s namespace, along with all their permittedvalues. The ordering of the lists is insignificant here. A particularplugin version MUST always return the same value (modulo ordering),irrespective of any runtime conditions.
get_supported_configs()->list[VariantFeatureConfigType] thatreturns a list of “variant feature configs” describing the variantfeatures within the plugin’s namespace that are compatible with thisparticular system, along with their values that are supported. Thevariant feature and value lists MUST be ordered from the mostpreferred to the least preferred, as they affectvariantordering.
The value returned byget_supported_configs() MUST be a subset ofthe feature names and values returned byget_all_configs() (moduloordering).
fromdataclassesimportdataclass@dataclassclassVariantFeatureConfig:name:strvalues:list[str]multi_value:bool# internal -- provided for illustrative purpose_MAX_VERSION=4_ALL_GPUS=["narf","poit","zort"]def_get_current_version()->int:"""Returns currently installed runtime version"""...# implementation not provideddef_is_gpu_available(codename:str)->bool:"""Is specified GPU installed?"""...# implementation not providedclassMyPlugin:namespace="example"# optional, defaults to Falseis_aot_plugin=False# all valid properties@staticmethoddefget_all_configs()->list[VariantFeatureConfig]:return[VariantFeatureConfig(# example :: gpu -- multi-valued, since the package# can target multiple GPUsname="gpu",# [narf, poit, zort]values=_ALL_GPUS,multi_value=True,),VariantFeatureConfig(# example :: min_version -- single-valued, since# there is always one minimumname="min_version",# [1, 2, 3, 4] (order doesn't matter)values=[str(x)forxinrange(1,_MAX_VERSION+1)],multi_value=False,),]# properties compatible with the system@staticmethoddefget_supported_configs()->list[VariantFeatureConfig]:current_version=_get_current_version()ifcurrent_versionisNone:# no runtime found, system not supported at allreturn[]return[VariantFeatureConfig(name="min_version",# [current, current - 1, ..., 1]values=[str(x)forxinrange(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 installedvalues=[xforxin_ALL_GPUSif_is_gpu_available(x)],multi_value=True,),]
The future versions of this specification, as well as third-partyextensions MAY introduce additional properties and methods on the plugininstances. The implementations SHOULD ignore additional attributes.
For best compatibility, all private attributes SHOULD be prefixed withan underscore (_) character to avoid incidental conflicts withfuture extensions.
As a build backend can’t determine whether the frontend supports variantwheels or not,PEP 517 andPEP 660 hooks MUST build non-variantwheels by default. Build backends MAY provide ways to request variantbuilds. This specification does not define any specific configuration.
When building variant wheels, build backends MUST verify variantmetadata for correctness, and they MUST NOT emit wheels withnonconformantvariant.json files. They SHOULD also query providersto determine whether variant properties requested by the user are valid,though they MAY permit skipping this verification and therefore emittingvariant wheels with potentially unknown properties.
variant_namespaces corresponding to the set of namespaces of allthe variant properties that the wheel variant was built for.
variant_features corresponding to the set ofnamespace::feature pairs of all the variant properties that thewheel variant was built for.
variant_properties corresponding to the set ofnamespace::feature::value tuples of all the variantproperties that the wheel variant was built for.
variant_label corresponding to the exact variant label that thewheel was built with. For the non-variant wheel, it is an emptystring.
The markers evaluating to sets of strings MUST be matched via theinornotin operator, e.g.:
# satisfied by any "foo :: * :: *" propertydep1;"foo"invariant_namespaces# satisfied by any "foo :: bar :: *" propertydep2;"foo :: bar"invariant_features# satisfied only by "foo :: bar :: baz" propertydep3;"foo :: bar :: baz"invariant_properties
Thevariant_label marker is a plain string:
# satisfied by the variant "foobar"dep4;variant_label=="foobar"# satisfied by any wheel other other than the null variant# (including the non-variant wheel)dep5;variant_label!="null"# satisfied by the non-variant wheeldep6;variant_label==""
Implementations MUST ignore differences in whitespace while matching thefeatures and properties.
Variant marker expressions MUST be evaluated against the variantproperties stored in the wheel being installed, not against the currentoutput of the provider plugins. If a non-variant wheel was selected orbuilt, all variant markers evaluate toFalse.
This section describes anOPTIONAL extension to the wheel variantspecification. Tools that choose to implement this feature MUST followthis specification. Tools that do not implement this feature MUST treatthe variants using it as incompatible, and SHOULD inform users when suchwheels are skipped.
The variant namespaceabi_dependency is reserved for expressing thatdifferent builds of the same version of a package are compatible withdifferent versions or version ranges of a dependency. This namespaceMUST NOT be used by any variant provider plugin, it MUST NOT be listedinproviders metadata, and can only appear in a built wheel variantproperty.
Within this namespace, zero or more properties can be used to expresscompatible dependency versions. For each property, the feature name MUSTbe thenormalized name of thedependency, whereas the value MUST be a valid release segment ofa public version identifier, as defined by theVersion specifiers specification.It MUST contain up to three version components, that are matched againstthe installed version same as the=={value}.* specifier. Notably,trailing zeroes match versions with fewer components (e.g.2.0matches release2 but not2.1). This also implies that theproperty values have different semantics than PEP 440 versions, inparticular2,2.0 and2.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 toindicate wheels compatible with multiple providing package versions,e.g.:
The primary source of information for Python package users should beinstaller documentation, supplemented by helpful informational messagesfrom command-line interface, and tutorials. Users without special needsshould not require any special variant awareness. Advanced users wouldspecifically need documentation on (provided the installer in questionimplements these features):
enabling untrusted provider plugins and the security implications ofthat
controlling provider usage, in particular enabling optional providers,disabling undesirable plugins or disabling variant usage in general
explicitly selecting variants, as well as controlling variantselection process
configuring variant selection for remote deployment targets, forexample using a static file generated on the target
The installer documentation may also be supplemented by documentationspecific to Python projects, in particular their installationinstructions.
For the transition period, during which some package managers do andsome do not support variant wheels, users need to be aware that certainfeatures may only be available with certain tools.
The primary source of information for maintainers of Python packagesshould be build backend documentation, supplemented by tutorials. Thedocumentation needs to indicate:
how to declare variant support inpyproject.toml
how to use variant environment markers to specify dependencies
how to build variant wheels
how to publish them and generate the*-variants.json file on localindexes
The maintainers will also need to peruse provider plugin documentation.They should also be aware which provider plugins are considered trustedby commonly used installers, and know the implications of usinguntrusted plugins. These materials may also be supplemented by genericdocuments explaining publishing variant wheels, along with specificexample use cases.
For the transition period, package maintainers need to be aware thatthey should still publish non-variant wheels for backwardscompatibility.
Existing installers MUST NOT accidentally install variant wheels, asthey require additional logic to determine whether a wheel is compatiblewith the user’s system. This is achieved byextending wheel filename through adding a-{variantlabel}component to the end of the filename, effectively causing variant wheelsto be rejected by common installer implementations. For backwardscompatibility, a non-variant wheel can be published in addition to thevariant wheels. It will be the only wheel supported by incompatibleinstallers, and the least preferred wheel for variant-compatibleinstallers.
Aside from this explicit incompatibility, the specification makesminimal and non-intrusive changes to the binary package format. Thevariant metadata is placed in a separate file in the.dist-infodirectory, which should be preserved by tools that are not concernedwith variants, limiting the necessary changes to updating the filenamevalidation algorithm (if there is one).
If the newvariant environment markers are used in wheeldependencies, these wheels will be incompatible with existing tools.This is a general problem with the design of environment markers, andnot specific to wheel variants. It is possible to work around thisproblem by partially evaluating environment markers at build time, andremoving the markers or dependencies specific to variant wheels from thenon-variant wheel.
Build backends produce non-variant wheels to preserve backwardscompatibility with existing frontends. Variant wheels can only be outputon explicit user request.
By using a separate*-variants.jsonfile for shared metadata,it ispossible to use variant wheels on an index that does not specificallysupport variant metadata. However, the index MUST permit distributingwheels that use the extended filename syntax and the JSON file.
Thevariantlib projectcontains a reference implementation of all the protocols and algorithmsintroduced in this PEP, as well as a command-line tool to convertwheels, generate the*-variants.json index and query plugins.
A client for installing variant wheels is implemented in auv branch.
TheWheel Variants monorepo includesexample implementations of provider plugins, as well as modifiedversions of build backends featuring variant wheel building support andmodified versions of some Python packages demonstrating variant wheeluses.
The support for additional variant properties could technically beimplemented without introducing provider plugins, but rather definingthe available properties and their discovery methods as part of thespecification, much like how wheel tags are implemented currently.However, the existing wheel tag logic already imposes a significantcomplexity on packaging tools that need to maintain the logic forgenerating supported tags, partially amortized by the data provided bythe Python interpreter itself.
Every new axis would be imposing even more effort on package managermaintainers, who would have to maintain an algorithm to determine theproperty compatibility. This algorithm could become quite complex,possibly needing to account for different platforms, hardware versionsand requiring more frequent updates than the one for platform tags. Thiswould also significantly increase the barrier towards adding new axesand therefore the risk of lack of feature parity between differentinstallers, as every new axis will be imposing additional maintenancecost.
For comparison, the plugin design essentially democratizes the variantproperties. Provider plugins can be maintained independently by peoplehaving the necessary knowledge and hardware. They can be updated asfrequently as necessary, independently of package managers. The decisionto use a particular provider falls entirely on the maintainer of packageneeding it, though they need to take into consideration that usingplugins that are not vetted by the common installers will inconveniencetheir users.
An alternative proposal was to publish the variants of the package asseparate projects on the index, along with the main package serving as a“resolver” directing to other variants via its metadata. For example, atorch package could indicate the conditions for usingtorch-cpu,torch-cu129, etc. subpackages.
Such an approach could possibly feature better backwards compatibilitywith existing tools. The changes would be limited to installers, andeven with pre-variant installers the users could explicitly requestinstalling a specific variant. However, it poses problems at multiplelevels.
The necessity of creating a new project for every variant will lead tothe proliferation of old projects, such astorch-cu123. While theuse of resolver package will ensure that only the modern variants areused, users manually installing packages and cross-package dependenciesmay accidentally be pinning to old variant projects, or even fall victimto name squatting. For comparison, the variant wheel proposal scopesvariants to each project version, and ensures that only the projectmaintainers can upload them.
Furthermore, it requires significant changes to the dependency resolverand package metadata formats. In particular, the dependency resolverwould need to query all “resolver” packages before performingresolution. It is unclear how to account for such variants whileperforming universal resolution. The one-to-one mapping betweendependencies and installed packages would be lost, as atorchdependency could effectively be satisfied bytorch-cu129.
This work would not have been possible without the contributions andfeedback of many people in the Python packaging community. Inparticular, we would like to credit the following individuals for theirhelp 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.