Python .whl files, or wheels, are a little-discussed part of Python, but they’ve been a boon to the installation process for Python packages. If you’ve installed a Python package using pip, then chances are that a wheel has made the installation faster and more efficient.
Wheels are a component of the Python ecosystem that helps to make package installs just work. They allow for faster installations and more stability in the package distribution process. In this tutorial, you’ll dive into what wheels are, what good they serve, and how they’ve gained traction and made Python even more of a joy to work with.
In this tutorial, you’ll learn:
- What wheels are and how they compare to source distributions
- How you can use wheels to control the package installation process
- How to create and distribute wheels for your own Python packages
You’ll see examples using popular open source Python packages from both the user’s and the developer’s perspective.
Free Bonus: Click here to get a Python Cheat Sheet and learn the basics of Python 3, like working with data types, dictionaries, lists, and Python functions.
Setup
To follow along, activate a virtual environment and make sure you have the latest versions of pip, wheel, and setuptools installed:
$ python -m venv env && source ./env/bin/activate
$ python -m pip install -U pip wheel setuptools
Successfully installed pip 20.1 setuptools-46.1.3 wheel-0.34.2
That’s all you need to experiment with installing and building wheels!
Python Packaging Made Better: An Intro to Python Wheels
Before you learn how to package a project into a wheel, it helps to know what using one looks like from the user’s side. It may sound backward, but a good way to learn how wheels work is to start by installing something that isn’t a wheel.
You can start this experiment by installing a Python package into your environment just as you might normally do. In this case, install uWSGI version 2.0.x:
1$ python -m pip install 'uwsgi==2.0.*'
2Collecting uwsgi==2.0.*
3 Downloading uwsgi-2.0.18.tar.gz (801 kB)
4 |████████████████████████████████| 801 kB 1.1 MB/s
5Building wheels for collected packages: uwsgi
6 Building wheel for uwsgi (setup.py) ... done
7 Created wheel for uwsgi ... uWSGI-2.0.18-cp38-cp38-macosx_10_15_x86_64.whl
8 Stored in directory: /private/var/folders/jc/8_hqsz0x1tdbp05 ...
9Successfully built uwsgi
10Installing collected packages: uwsgi
11Successfully installed uwsgi-2.0.18
To fully install uWSGI, pip progresses through several distinct steps:
- On line 3, it downloads a TAR file (tarball) named
uwsgi-2.0.18.tar.gzthat’s been compressed with gzip. - On line 6, it takes the tarball and builds a
.whlfile through a call tosetup.py. - On line 7, it labels the wheel
uWSGI-2.0.18-cp38-cp38-macosx_10_15_x86_64.whl. - On line 10, it installs the actual package after having built the wheel.
The tar.gz tarball that pip retrieves is a source distribution, or sdist, rather than a wheel. In some ways, a sdist is the opposite of a wheel.
Note: If you see an error with the uWSGI installation, you may need to install the Python development headers.
A source distribution contains source code. That includes not only Python code but also the source code of any extension modules (usually in C or C++) bundled with the package. With source distributions, extension modules are compiled on the user’s side rather than the developer’s.
Source distributions also contain a bundle of metadata sitting in a directory called <package-name>.egg-info. This metadata helps with building and installing the package, but user’s don’t really need to do anything with it.
From the developer’s perspective, a source distribution is what gets created when you run the following command:
$ python setup.py sdist
Now try installing a different package, chardet:
1$ python -m pip install 'chardet==3.*'
2Collecting chardet
3 Downloading chardet-3.0.4-py2.py3-none-any.whl (133 kB)
4 |████████████████████████████████| 133 kB 1.5 MB/s
5Installing collected packages: chardet
6Successfully installed chardet-3.0.4
You can see a noticeably different output than the uWSGI install.
Installing chardet downloads a .whl file directly from PyPI. The wheel name chardet-3.0.4-py2.py3-none-any.whl follows a specific naming convention that you’ll see later. What’s more important from the user’s perspective is that there’s no build stage when pip finds a compatible wheel on PyPI.
From the developer’s side, a wheel is the result of running the following command:
$ python setup.py bdist_wheel
Why does uWSGI hand you a source distribution while chardet provides a wheel? You can see the reason for this by taking a look at each project’s page on PyPI and navigating to the Download files area. This section will show you what pip actually sees on the PyPI index server:
- uWSGI provides only a source distribution (
uwsgi-2.0.18.tar.gz) for reasons related to the complexity of the project. - chardet provides both a wheel and a source distribution, but
pipwill prefer the wheel if it’s compatible with your system. You’ll see how that compatibility is determined later on.
Another example of the compatibility check used for wheel installation is psycopg2, which provides a wide set of wheels for Windows but doesn’t provide any for Linux or macOS clients. This means that pip install psycopg2 could fetch a wheel or a source distribution depending on your specific setup.
To avoid these types of compatibility issues, some packages offer multiple wheels, with each wheel geared toward a specific Python implementation and underlying operating system.
So far, you’ve seen some of the visible distinctions between a wheel and sdist, but what matters more is the impact those differences have on the installation process.
Wheels Make Things Go Fast
Above, you saw a comparison of an installation that fetches a prebuilt wheel and one that downloads a sdist. Wheels make the end-to-end installation of Python packages faster for two reasons:
- All else being equal, wheels are typically smaller in size than source distributions, meaning they can move faster across a network.
- Installing from wheels directly avoids the intermediate step of building packages off of the source distribution.
It’s almost guaranteed that the chardet install occurred in a fraction of the time required for uWSGI. However, that’s arguably an unfair apples-to-oranges comparison since chardet is a significantly smaller and less complex package. With a different command, you can create a more direct comparison that will demonstrate just how much of a difference wheels make.
You can make pip ignore its inclination towards wheels by passing the --no-binary option:
$ time python -m pip install \
--no-cache-dir \
--force-reinstall \
--no-binary=:all: \
cryptography
This command times the installation of the cryptography package, telling pip to use a source distribution even if a suitable wheel is available. Including :all: makes the rule apply to cryptography and all of its dependencies.
On my machine, this takes around thirty-two seconds from start to finish. Not only does the install take a long time, but building cryptography also requires that you have the OpenSSL development headers present and available to Python.
Note: With --no-binary, you may very well see an error about missing header files required for the cryptography install, which is part of what can make using source distributions frustrating. If so, the installation section of the cryptography docs advises on which libraries and header files you’ll need for a particular operating system.
Now you can reinstall cryptography, but this time make sure that pip uses wheels from PyPI. Because pip will prefer a wheel, this is similar to just calling pip install with no arguments at all. But in this case, you can make the intent explicit by requiring a wheel with --only-binary:
$ time python -m pip install \
--no-cache-dir \
--force-reinstall \
--only-binary=cryptography \
cryptography
This option takes just over four seconds, or one-eighth the time that it took when using only source distributions for cryptography and its dependencies.
What Is a Python Wheel?
A Python .whl file is essentially a ZIP (.zip) archive with a specially crafted filename that tells installers what Python versions and platforms the wheel will support.
A wheel is a type of built distribution. In this case, built means that the wheel comes in a ready-to-install format and allows you to skip the build stage required with source distributions.
Note: It’s worth mentioning that despite the use of the term built, a wheel doesn’t contain .pyc files, or compiled Python bytecode.
A wheel filename is broken down into parts separated by hyphens:
{dist}-{version}(-{build})?-{python}-{abi}-{platform}.whl
Each section in {brackets} is a tag, or a component of the wheel name that carries some meaning about what the wheel contains and where the wheel will or will not work.
Here’s an illustrative example using a cryptography wheel:
cryptography-2.9.2-cp35-abi3-macosx_10_9_x86_64.whl
cryptography distributes multiple wheels. Each wheel is a platform wheel, meaning it supports only specific combinations of Python versions, Python ABIs, operating systems, and machine architectures. You can break down the naming convention into parts:
-
cryptographyis the package name. -
2.9.2is the package version ofcryptography. A version is a PEP 440-compliant string such as2.9.2,3.4, or3.9.0.a3. -
cp35is the Python tag and denotes the Python implementation and version that the wheel demands. Thecpstands for CPython, the reference implementation of Python, while the35denotes Python 3.5. This wheel wouldn’t be compatible with Jython, for instance. -
abi3is the ABI tag. ABI stands for application binary interface. You don’t really need to worry about what it entails, butabi3is a separate version for the binary compatibility of the Python C API. -
macosx_10_9_x86_64is the platform tag, which happens to be quite a mouthful. In this case it can be broken down further into sub-parts:macosxis the macOS operating system.10_9is the macOS developer tools SDK version used to compile the Python that in turn built this wheel.x86_64is a reference to x86-64 instruction set architecture.
The final component isn’t technically a tag but rather the standard .whl file extension. Combined, the above components indicate the target machine that this cryptography wheel is designed for.
Now let’s turn to a different example. Here’s what you saw in the above case for chardet:
chardet-3.0.4-py2.py3-none-any.whl
You can break this down into its tags:
chardetis the package name.3.0.4is the package version of chardet.py2.py3is the Python tag, meaning the wheel supports Python 2 and 3 with any Python implementation.noneis the ABI tag, meaning the ABI isn’t a factor.anyis the platform. This wheel will work on virtually any platform.
The py2.py3-none-any.whl segment of the wheel name is common. This is a universal wheel that will install with Python 2 or 3 on any platform with any ABI. If the wheel ends in none-any.whl, then it’s very likely a pure-Python package that doesn’t care about a specific Python ABI or CPU architecture.
Another example is the jinja2 templating engine. If you navigate to the downloads page for the Jinja 3.x alpha release, then you’ll see the following wheel:
Jinja2-3.0.0a1-py3-none-any.whl
Notice the lack of py2 here. This is a pure-Python project that will work on any Python 3.x version, but it’s not a universal wheel because it doesn’t support Python 2. Instead, it’s called a pure-Python wheel.
Note: In 2020, a number of projects are also dropping support for Python 2, which reached end-of-life (EOL) on January 1, 2020. Jinja version 3.x dropped Python 2 support in February 2020.
Here are a few more examples of .whl names distributed for some popular open source packages:
| Wheel | What It Is |
|---|---|
PyYAML-5.3.1-cp38-cp38-win_amd64.whl |
PyYAML for CPython 3.8 on Windows with AMD64 (x86-64) architecture |
numpy-1.18.4-cp38-cp38-win32.whl |
NumPy for CPython 3.8 on Windows 32-bit |
scipy-1.4.1-cp36-cp36m-macosx_10_6_intel.whl |
SciPy for CPython 3.6 on macOS 10.6 SDK with fat binary (multiple instruction sets) |
Now that you have a thorough understanding of what wheels are, it’s time to talk about what good they serve.
Advantages of Python Wheels
Here’s a testament to wheels from the Python Packaging Authority (PyPA):
Not all developers have the right tools or experiences to build these components written in these compiled languages, so Python created the wheel, a package format designed to ship libraries with compiled artifacts. In fact, Python’s package installer,
pip, always prefers wheels because installation is always faster, so even pure-Python packages work better with wheels. (Source)
A fuller description is that wheels benefit both users and maintainers of Python packages alike in a handful of ways:
-
Wheels install faster than source distributions for both pure-Python packages and extension modules.
-
Wheels are smaller than source distributions. For example, the
sixwheel is about one-third the size of the corresponding source distribution. This differential becomes even more important when you consider that apip installfor a single package may actually kick off downloading a chain of dependencies. -
Wheels cut
setup.pyexecution out of the equation. Installing from a source distribution runs whatever is contained in that project’ssetup.py. As pointed out by PEP 427, this amounts to arbitrary code execution. Wheels avoid this altogether. -
There’s no need for a compiler to install wheels that contain compiled extension modules. The extension module comes included with the wheel targeting a specific platform and Python version.
-
pipautomatically generates.pycfiles in the wheel that match the right Python interpreter. -
Wheels provide consistency by cutting many of the variables involved in installing a package out of the equation.
You can use a project’s Download files tab on PyPI to view the different distributions that are available. For example, pandas distributes a wide array of wheels.
Telling pip What to Download
It’s possible to exert fine-grained control over pip and tell it which format to prefer or avoid. You can use the --only-binary and --no-binary options to do this. You saw these used in an earlier section on installing the cryptography package, but it’s worth taking a closer look at what they do:
$ pushd "$(mktemp -d)"
$ python -m pip download --only-binary :all: --dest . --no-cache six
Collecting six
Downloading six-1.14.0-py2.py3-none-any.whl (10 kB)
Saved ./six-1.14.0-py2.py3-none-any.whl
Successfully downloaded six
In this example, you change to a temporary directory to store the download with pushd "$(mktemp -d)". You use pip download rather than pip install so that you can inspect the resulting wheel, but you can replace download with install while keeping the same set of options.
You download the six module with several flags:
--only-binary :all:tellspipto constrain itself to using wheels and ignore source distributions. Without this option,pipwill only prefer wheels but will fall back to source distributions in some scenarios.--dest .tellspipto downloadsixto the current directory.--no-cachetellspipnot to look in its local download cache. You use this option just to illustrate a live download from PyPI since it’s likely you do have asixcache somewhere.
I mentioned earlier that a wheel file is essentially a .zip archive. You can take this statement literally and treat wheels as such. For instance, if you want to view a wheel’s contents, you can use unzip:
$ unzip -l six*.whl
Archive: six-1.14.0-py2.py3-none-any.whl
Length Date Time Name
--------- ---------- ----- ----
34074 01-15-2020 18:10 six.py
1066 01-15-2020 18:10 six-1.14.0.dist-info/LICENSE
1795 01-15-2020 18:10 six-1.14.0.dist-info/METADATA
110 01-15-2020 18:10 six-1.14.0.dist-info/WHEEL
4 01-15-2020 18:10 six-1.14.0.dist-info/top_level.txt
435 01-15-2020 18:10 six-1.14.0.dist-info/RECORD
--------- -------
37484 6 files
six is a special case: it’s actually a single Python module rather than a complete package. Wheel files can also be significantly more complex, as you’ll see later on.
In contrast to --only-binary, you can use --no-binary to do the opposite:
$ python -m pip download --no-binary :all: --dest . --no-cache six
Collecting six
Downloading six-1.14.0.tar.gz (33 kB)
Saved ./six-1.14.0.tar.gz
Successfully downloaded six
$ popd
The only change in this example is the switch to --no-binary :all:. This tells pip to ignore wheels even if they’re available and instead download a source distribution.
When might --no-binary be useful? Here are a few cases:
-
The corresponding wheel is broken. This is an irony of wheels. They’re designed to make things break less often, but in some cases a wheel can be misconfigured. In this case, downloading and building the source distribution for yourself may be a working alternative.
-
You want to apply a small change or patch file to the project and then install it. This is an alternative to cloning the project from its version control system URL.
You can also use the flags described above with pip install. Additionally, instead of :all:, which will apply the --only-binary rule not just to the package you’re installing but to all of its dependencies, you can pass --only-binary and --no-binary a list of specific packages to apply that rule to.
Here are a few examples for installing the URL library yarl. It contains Cython code and depends on multidict, which contains pure C code. There are several options to strictly use or strictly ignore wheels for yarl and its dependencies:
$ # Install `yarl` and use only wheels for yarl and all dependencies
$ python -m pip install --only-binary :all: yarl
$ # Install `yarl` and use wheels only for the `multidict` dependency
$ python -m pip install --only-binary multidict yarl
$ # Install `yarl` and don't use wheels for yarl or any dependencies
$ python -m pip install --no-binary :all: yarl
$ # Install `yarl` and don't use wheels for the `multidict` dependency
$ python -m pip install --no-binary multidict yarl
In this section, you got a glimpse of how to fine-tune the distribution types that pip install will use. While a regular pip install should work with no options, it’s helpful to know these options for special cases.
The manylinux Wheel Tag
Linux comes in many variants and flavors, such as Debian, CentOS, Fedora, and Pacman. Each of these may use slight variations in shared libraries, such as libncurses, and core C libraries, such as glibc.
If you’re writing a C/C++ extension, then this could create a problem. A source file written in C and compiled on Ubuntu Linux isn’t guaranteed to be executable on a CentOS machine or an Arch Linux distribution. Do you need to build a separate wheel for each and every Linux variant?
Luckily, the answer is no, thanks to a specially designed set of tags called the manylinux platform tag family. There are currently three variations:
-
manylinux1is the original format specified in PEP 513. -
manylinux2010is an update specified in PEP 571 that upgrades to CentOS 6 as the underlying OS on which the Docker images are based. The rationale is that CentOS 5.11, which is where the list of allowed libraries inmanylinux1comes from, reached EOL in March 2017 and stopped receiving security patches and bug fixes. -
manylinux2014is an update specified in PEP 599 that upgrades to CentOS 7 since CentOS 6 is scheduled to reach EOL in November 2020.
You can find an example of manylinux distributions within the pandas project. Here are two (out of many) from the list of available pandas downloads from PyPI:
pandas-1.0.3-cp37-cp37m-manylinux1_x86_64.whl
pandas-1.0.3-cp37-cp37m-manylinux1_i686.whl
In this case, pandas has built manylinux1 wheels for CPython 3.7 supporting both x86-64 and i686 architectures.
At its core, manylinux is a Docker image built off a certain version of the CentOS operating system. It comes bundled with a compiler suite, multiple versions of Python and pip, and an allowed set of shared libraries.
Note: The term allowed indicates a low-level library that is assumed to be present by default on almost all Linux systems. The idea is that the dependency should exist on the base operating system without the need for an additional install.
As of mid-2020, manylinux1 is still the predominant manylinux tag. One reason for this might just be habit. Another might be that support on the client (user) side for manylinux2010 and above is limited to more recent versions of pip:
| Tag | Requirement |
|---|---|
manylinux1 |
pip 8.1.0 or later |
manylinux2010 |
pip 19.0 or later |
manylinux2014 |
pip 19.3 or later |
In other words, if you’re a package developer building manylinux2010 wheels, then someone using your package will need pip 19.0 (released in January 2019) or later to let pip find and install manylinux2010 wheels from PyPI.
Luckily, virtual environments have become more common, meaning that developers can update a virtual environment’s pip without touching the system pip. This isn’t always the case, however, and some Linux distributions still ship with outdated versions of pip.
All that is to say, if you’re installing Python packages on a Linux host, then consider yourself fortunate if the package maintainer has gone out of their way to create manylinux wheels. This will almost guarantee a hassle-free installation of the package regardless of your specific Linux variant or version.
Caution: Be advised that PyPI wheels don’t work on Alpine Linux (or BusyBox). This is because Alpine uses