When distributing a Python package to the Python Package Index (PyPI), you must provide at least one built distribution format. The two primary formats are the source distribution (sdist) and the wheel (bdist_wheel). Understanding the distinction between them, their creation process, and their appropriate use is crucial for efficient and reliable distribution.

A source distribution, created with python -m build --sdist, is the most basic and universal distribution format. It contains your package’s source code, setup.py or pyproject.toml, and any other files specified in your MANIFEST.in. When a user installs an sdist using pip, the package must be built on their machine. This process involves executing the setup.py script (for legacy packages) or invoking the build backend specified in pyproject.toml (for modern PEP 517/518 packages), which ultimately compiles any extension modules and places the files in the correct location for the target environment. The key advantage of an sdist is its universality; it can be installed on any platform with a compatible build toolchain. However, this is also its main drawback: the end user must have all necessary compilers (like a C compiler for packages with C extensions) and development headers (like Python.h) installed, which can be a significant barrier to installation.

Creating a Source Distribution (sdist)

The modern tool for building distributions is build. It reads the [build-system] table in your pyproject.toml to determine the correct build backend (e.g., setuptools, flit, hatchling).

# Example pyproject.toml for a setuptools project
[build-system]
requires = ["setuptools>=61.0.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "my-awesome-package"
version = "1.0.0"
description = "A package that does awesome things"

To build the sdist, run the following command from the directory containing your pyproject.toml:

python -m build --sdist

This creates a .tar.gz file in the dist/ directory (e.g., my-awesome-package-1.0.0.tar.gz). You can inspect its contents to ensure all necessary files are included.

Creating a Built Distribution (Wheel)

A wheel (.whl file), created with python -m build --wheel, is a built distribution format. Unlike an sdist, a wheel is a ready-to-install archive. It is pre-built; compilation of extension modules, processing of resources, and generation of metadata all happen on the developer’s or CI/CD system’s machine, not the end user’s. This makes installation incredibly fast and reliable for the user, as it eliminates the need for a build environment. Wheels are also often smaller than sdists as they don’t include tests, documentation, or other non-essential files. The critical limitation is that wheels are often platform-specific. A wheel built for Linux on an x86-64 architecture will not work on macOS or Windows, or on a Linux ARM machine.

Platform-Specific and Universal Wheels

Wheels are categorized by their compatibility. A universal wheel is a pure-Python wheel that works on any Python version and platform. It’s tagged as py3-none-any.whl. If your package contains no C extensions and supports both Python 2 and 3, you can create a universal wheel by setting the universal option in setup.cfg for setuptools.

# setup.cfg
[bdist_wheel]
universal = 1

If your package includes C extensions or is otherwise platform-specific, bdist_wheel will generate a platform wheel (e.g., my_package-1.0.0-cp39-cp39-macosx_10_15_x86_64.whl). The filename encodes the Python version, ABI, and platform it was built for. This specificity ensures the wheel will only be installed on a compatible system.

Best Practices and Common Pitfalls

  1. Always Provide Both: For maximum compatibility, you should upload both an sdist and a wheel to PyPI. pip will prefer a compatible wheel if one exists, falling back to the sdist only if necessary. This provides a smooth experience for the vast majority of users while still supporting edge cases.
  2. The pyproject.toml Mandate: Modern packaging tools rely on pyproject.toml. Ensure your [build-system] section is correct. A common pitfall is missing the wheel dependency here, which can cause the build process to fail.
  3. MANIFEST.in vs. package_data: Understand what each file controls. MANIFEST.in dictates what goes into the source distribution. package_data (in setup.py) or [tool.setuptools.package-data] (in pyproject.toml) dictates what non-Python files are installed alongside your package from both an sdist and a wheel. A frequent mistake is adding a file to MANIFEST.in but forgetting package_data, resulting in a file that is distributed but not installed.
  4. Testing Your Distributions: Before uploading, always test your built distributions in a clean virtual environment. Install the local .whl file to verify it works correctly.
    python -m pip install path/to/dist/my_awesome_package-1.0.0-py3-none-any.whl
    
    For the sdist, test that it can be built and installed in an environment with minimal dependencies.
    python -m pip install --no-binary :all: path/to/dist/my_awesome_package-1.0.0.tar.gz
    
  5. Versioning and Filenames: Never manually rename built distribution files. The build tools generate the filenames based on your project’s name and version. Uploading a file with an incorrect name will break pip’s ability to resolve dependencies and install it.