Packaging, Wheel Distribution, and Publishing

Updated

September 7, 2026

Packaging, Wheel Distribution, and Publishing

After reading this chapter, you will master the modern Python packaging standards (PEP 517, PEP 518, PEP 621), author declarative pyproject.toml specifications, configure executable CLI console scripts, build standard source distributions (.tar.gz) and built wheel packages (.whl) with uv build, and inspect package distribution metadata.

Mental model

In legacy Python, packaging relied on imperative setup.py scripts that executed arbitrary Python code during installation. Modern packaging uses declarative TOML metadata (pyproject.toml) and isolated build frontends:

Modern Packaging Architecture:
  pyproject.toml (PEP 621 Metadata & Build Backend)
        │
        ▼ uv build
  ┌─────────────────────────┴─────────────────────────┐
  ▼                                                   ▼
Source Distribution (sdist):                 Built Wheel (.whl):
  mypkg-0.1.0.tar.gz                           mypkg-0.1.0-py3-none-any.whl
  ├── Raw Python source code                   ├── Pre-built package files
  └── pyproject.toml                           └── mypkg-0.1.0.dist-info/
                                                   ├── METADATA
                                                   ├── WHEEL
                                                   ├── entry_points.txt
                                                   └── RECORD (SHA-256 hashes)

A wheel (.whl) is a ZIP archive formatted with exact installation targets. Installing a wheel is simply an unpack and copy operation into site-packages with zero build step execution.


Minimal example

Save as package_builder_demo.py:

# package_builder_demo.py
import tempfile
import zipfile
from pathlib import Path

SAMPLE_PYPROJECT = """
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "cluster-agent"
version = "1.0.0"
description = "Automated cluster monitoring telemetry agent."
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "rich>=13.0.0",
]

[project.scripts]
cluster-agent = "cluster_agent.cli:main"
"""

def generate_package_skeleton(root: Path) -> None:
    (root / "pyproject.toml").write_text(SAMPLE_PYPROJECT.strip())
    (root / "README.md").write_text("# Cluster Agent\nHigh-speed cluster agent.")
    
    pkg_dir = root / "cluster_agent"
    pkg_dir.mkdir()
    (pkg_dir / "__init__.py").write_text('__version__ = "1.0.0"\n')
    (pkg_dir / "cli.py").write_text("""
def main() -> None:
    print("Cluster agent running.")
""")

def main() -> None:
    with tempfile.TemporaryDirectory() as tmpdir:
        root = Path(tmpdir)
        generate_package_skeleton(root)
        print("Generated modern package skeleton with pyproject.toml:")
        for path in sorted(root.rglob("*")):
            if path.is_file():
                print(f"  - {path.relative_to(root)}")

if __name__ == "__main__":
    main()

Run via uv run python package_builder_demo.py:

Generated modern package skeleton with pyproject.toml:
  - README.md
  - cluster_agent/__init__.py
  - cluster_agent/cli.py
  - pyproject.toml

Worked examples

Case 1: Defining CLI Console Scripts via [project.scripts]

When users install your package, they often want a global command-line command (my-tool) available in their shell. Modern pyproject.toml declares this declaratively:

[project.scripts]
# CLI command name = "module_path:function_to_call"
infra-cli = "infra_pkg.cli:main_entrypoint"

When installed, pip or uv automatically generates an executable binary wrapper in the environment’s bin/ directory that executes main_entrypoint().

Case 2: Building Wheels with uv build

Executing uv build reads pyproject.toml, prepares an isolated build environment, and outputs standards-compliant artifacts into dist/:

# Execute build
uv build

# Output:
# Successfully built dist/cluster_agent-1.0.0.tar.gz
# Successfully built dist/cluster_agent-1.0.0-py3-none-any.whl

Case 3: Inspecting Wheel Metadata (.dist-info)

Because a .whl file is a ZIP archive, you can inspect its metadata directly:

# inspect_wheel.py
import zipfile

def inspect_wheel_metadata(wheel_path: str) -> None:
    with zipfile.ZipFile(wheel_path, "r") as zf:
        metadata_files = [name for name in zf.namelist() if name.endswith("METADATA")]
        if metadata_files:
            meta_text = zf.read(metadata_files[0]).decode("utf-8")
            print("--- Package METADATA Content ---")
            for line in meta_text.splitlines()[:10]:
                print(line)

if __name__ == "__main__":
    print("Wheel metadata inspection helper loaded.")

Run:

uv run python inspect_wheel.py

Output:

Wheel metadata inspection helper loaded.

Pitfalls

Pitfall 1: Relying on Legacy setup.py

Modern toolchains (uv, pip, build) deprecate setup.py in favor of declarative pyproject.toml. Imperative setup.py scripts introduce build-time security vulnerabilities and prevent static dependency analysis.

Pitfall 2: Missing Package Data Files

If your package relies on static templates, HTML files, or YAML configs, you must explicitly declare them in your build backend configuration:

# Example for hatchling backend:
[tool.hatch.build.targets.wheel]
packages = ["cluster_agent"]
include = [
    "cluster_agent/templates/*.html",
]

Otherwise, wheels will package only .py source files, causing FileNotFoundError at runtime.


Exercises

  1. Write a minimal pyproject.toml using hatchling as the build backend, declaring a console script entry point.
  2. Build a wheel package using uv build in a temporary test directory and verify the files generated in dist/.
  3. Unzip a .whl archive and verify the contents of its entry_points.txt and RECORD files.
  4. Configure optional dependency groups ([project.optional-dependencies]) for dev and test suites.

Further reading

  • PEP 517: A build-system independent format for source trees.
  • PEP 518: Specifying Minimum Build System Requirements for Python Projects.
  • PEP 621: Storing project metadata in pyproject.toml.
  • Python Packaging Authority (PyPA): Packaging Python Projects.