How It Works

This page explains the technical details of how rind works.

PEP 517 Build Backend

rind is a PEP 517 build backend. When you run python -m build, the build frontend:

  1. Creates an isolated virtual environment

  2. Installs the packages listed in build-system.requires

  3. Imports the module specified in build-system.build-backend

  4. Calls the appropriate hook functions (build_wheel, build_sdist)

The backend implements these hooks to generate packages without any Python code.

Wheel Structure

A typical Python wheel contains:

mypackage-1.0.0-py3-none-any.whl
├── mypackage/
│   ├── __init__.py
│   └── module.py
└── mypackage-1.0.0.dist-info/
    ├── METADATA
    ├── WHEEL
    └── RECORD

A rind wheel contains only the metadata:

mypackage-1.0.0-py3-none-any.whl
└── mypackage-1.0.0.dist-info/
    ├── METADATA
    ├── WHEEL
    └── RECORD

This is perfectly valid according to the wheel specification. When pip installs this wheel, it:

  1. Records that mypackage==1.0.0 is installed

  2. Installs all packages listed in Requires-Dist

  3. Leaves no Python files (because there are none)

Version Pinning

rind automatically uses whatever versioning system your core package uses. It reads the core package’s pyproject.toml and detects:

  • Static versions: If version is in [project] and not in dynamic, it’s used directly.

  • setuptools_scm / hatch-vcs: If detected in build requirements or tool config, rind calls setuptools_scm directly for fast version detection.

  • Other backends: As a fallback, rind calls the core’s build backend via prepare_metadata_for_build_wheel to get the version.

When you tag a release and build both packages:

$ git tag v1.2.3
$ python -m build .        # In repo root
$ python -m build meta/    # In meta/ directory

Both builds use the same version source, so both get version 1.2.3.

The metapackage’s METADATA file contains:

Requires-Dist: mypackage-core==1.2.3

This ensures that pip install mypackage==1.2.3 always installs mypackage-core==1.2.3.

Important

Version pinning only works correctly when both packages are built from the same git commit. Always build and release them together.

Metadata Inheritance

When inherit-metadata is true (the default), the backend:

  1. Reads the core package’s pyproject.toml (via core-path)

  2. Extracts the [project] table

  3. Uses those values as defaults for the metapackage

The inheritance priority is:

  1. [tool.rind] values (highest priority)

  2. [project] values in the metapackage’s own pyproject.toml

  3. Inherited values from core’s pyproject.toml (lowest priority)

Set inherit-metadata = false to disable this behavior and only use the core’s pyproject.toml for version detection.

Sdist Structure

When building a wheel from an sdist, the core package’s pyproject.toml isn’t available (the sdist is extracted to a temporary directory).

To handle this, rind generates a resolved pyproject.toml in the sdist:

  1. During build_sdist, all metadata is computed from the core package

  2. A new pyproject.toml is generated with all values hardcoded (version, dependencies, inherited metadata, etc.)

  3. This resolved file has no core-path setting, which tells rind to read values directly from [project] instead of computing them

This ensures wheels built from sdists have the same metadata as wheels built directly from the repository, without needing access to the core package.

Import Name vs Package Name

A key feature of this setup is that the import name stays the same while the package name changes.

Consider this structure:

myproject/
├── pyproject.toml      # name = "mypackage-core"
└── src/
    └── mypackage/      # Import name: mypackage
        └── __init__.py

The package installed via pip is mypackage-core, but the directory containing the code is mypackage/, so users write:

import mypackage  # Works!

When mypackage (the metapackage) is installed, it pulls in mypackage-core, which provides the mypackage/ directory. The metapackage itself provides no Python files, so there’s no conflict.

Comparison with Alternatives

Why not use extras on a single package?

You could use optional dependencies:

[project]
name = "mypackage"
dependencies = ["numpy"]  # minimal

[project.optional-dependencies]
recommended = ["pandas", "matplotlib"]

Users install with pip install mypackage[recommended].

The metapackage approach is better when:

  • You want the “batteries included” experience to be the default

  • The package name without brackets should give the full experience

  • You want clear separation between “essential” and “recommended”

Why not two repositories?

You could maintain separate repos for core and meta packages. The single-repo approach is better because:

  • Versions are automatically synchronized via git tags

  • No coordination needed between repos for releases

  • Single source of truth for metadata

  • Easier CI/CD setup

Standalone Mode

rind also supports a standalone mode for creating metapackages without a core package. In this mode:

  • All metadata is specified directly in [project]

  • No core-path is needed

  • Version must be static (no dynamic versioning)

  • No metadata inheritance

This is useful for creating curated dependency bundles like “my-data-science-stack” that group related packages together.

Limitations

  • Both packages must be released together (core-package mode only): Since versions are synchronized, you can’t release one without the other.

  • No code in metapackage: The metapackage cannot contain any Python code. If you need wrapper code, it should go in the core package.

  • Static version in standalone mode: Standalone metapackages require a static version in [project].