Reference · Packaging

Packaging commands and layouts

Compressed from Lesson 0002. Built to be printed and kept beside the keyboard.

Inspect a built distribution

QuestionCommand
What is in the wheel? unzip -l dist/*.whl
What is in the sdist? tar tzvf dist/*.tar.gz
Read a file without unpacking unzip -p dist/*.whl pippen-1.0.0.dist-info/METADATA
Does it carry a given file? unzip -l dist/*.whl | grep py.typed
Build both artifacts uv build
Check metadata before upload uvx twine check dist/*

Where things live

RepositorysdistWheel
Package code src/pippen/ pippen-1.0.0/src/pippen/ pippen/
Shipped tables src/pippen/_data/ pippen-1.0.0/src/pippen/_data/ pippen/_data/
Metadata pyproject.toml pippen-1.0.0/PKG-INFO pippen-1.0.0.dist-info/
Tests tests/ included absent

The wheel is the installed layout. src/ exists to keep the working directory off sys.path during development and has no meaning after installation.

Inside dist-info

FileHoldsUsed by
METADATAName, version, dependencies, READMEpip resolver, PyPI page
RECORDEvery installed file, with hash and sizepip uninstall
WHEELWheel format version, whether pure PythonThe installer
entry_points.txtConsole scripts, such as pippenScript generation at install

Filename grammar

pippen-1.0.0-py3-none-any.whl
│      │     │   │    │
│      │     │   │    └─ platform tag: any OS
│      │     │   └────── ABI tag: no compiled extensions
│      │     └────────── Python tag: any Python 3
│      └──────────────── version
└─────────────────────── distribution name

A wheel tagged py3-none-any is a pure-Python wheel: one file that installs everywhere. A package with compiled C would instead publish one wheel per platform, such as cp310-manylinux_2_17_x86_64.

Declaring package data, hatchling

[tool.hatch.build.targets.wheel]
packages = ["src/pippen"]
artifacts = ["src/pippen/_data/*.parquet"]

[tool.hatch.build.targets.sdist]
include = ["src/", "tests/", "README.md", "LICENSE"]

The two targets are configured separately and do not inherit from each other. Declaring one and forgetting the other yields a package that works from a wheel and fails for anyone building from source.

Reading package data at runtime

from importlib.resources import as_file, files

resource = files("pippen").joinpath("_data").joinpath(filename)
with as_file(resource) as path:
    return pd.read_parquet(path)

Never compute a path from __file__. An installed package is not guaranteed to be a directory of loose files: it can be inside a zip, and files() asks the loader that actually imported the module where its resources are. as_file() materialises a real path when a library needs one, and cleans up afterwards if extraction was required.

Version numbers

ChangeBumpPromise
Bug fix, no API change0.1.1Safe upgrade
New function added0.2.0Safe upgrade, more available
Signature or behaviour changed1.0.0Callers may break

Below 1.0.0 the guarantee is weaker by convention: a minor bump may break callers, which is what 0.x communicates. That is why the README says the API is 0.x and may still change.

A version published to PyPI can be yanked or deleted, and can never be re-uploaded under the same number. Publishing is append-only.