SuperAPI AI Directory

packaging-python-libraries

Coding & development

View repository

Updated 10-16 01:09

About this skill

Python Library Packaging

pyproject.toml Essentials

[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "my-package"
version = "1.0.0"
description = "Short description"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
dependencies = []

[project.optional-dependencies] dev = ["pytest>=7.0", "ruff>=0.1", "mypy>=1.0"]

[project.urls] Homepage = "https://github.com/user/package" Documentation = "https://package.readthedocs.io"

[project.scripts] mycli = "my_package.cli:main"

[tool.setuptools.packages.find] where = ["src"]


## Building

```bash
uv build                     # Creates sdist + wheel in dist/
uvx twine check dist/*       # Validate metadata

Publishing to PyPI

Prefer trusted publishing from CI (no stored token) — see the CI section below. For a manual publish, upload with twine via uvx:

uvx twine upload --repository testpypi dist/*  # Test first
uvx twine upload dist/*                         # Production (uses a PyPI token)

GitHub Actions (Trusted Publishing)

Publishing is automated by the canonical tag-triggered release workflow in the managing-python-releases skill — one pipeline builds with uv and publishes via trusted publishing. See ../release-management/AUTOMATION.md; don't maintain a second copy here.

Dependency Best Practices

# DO: Minimum versions
dependencies = ["requests>=2.28", "click>=8.0"]

# DON'T: Exact pins (locks users)
dependencies = ["requests==2.28.1"]

# DO: Optional for features
[project.optional-dependencies]
cli = ["click>=8.0"]

Including Package Data

[tool.setuptools.package-data]
my_package = ["py.typed", "data/*.json"]
from importlib.resources import files
data = files("my_package.data").joinpath("file.json").read_text()

Direct-Reference Dependencies Need a Real Build

A dependency such as toolkit @ https://example.com/toolkit.whl may resolve in an install dry run without invoking the build backend. With Hatchling, the real wheel build then fails unless direct references are explicitly allowed:

[project]
dependencies = [
    "toolkit @ https://example.com/releases/toolkit.whl",
]

[tool.hatch.metadata]
allow-direct-references = true

Do not use uv pip install --dry-run . as the packaging check for this case. It can report success without building the project. Run uv build (or a real uv pip install .) so the configured backend validates the metadata:

uv build
uvx twine check dist/*

For detailed templates, see:

Verify the Built Artifact (a green build is not a correct wheel)

uv build succeeding tells you the backend ran, not that the wheel contains your code. Build backends select files via config — hatchling's [tool.hatch.build.targets.wheel] (only-include / packages / include), setuptools' [tool.setuptools.packages.find]. Get that config wrong and the backend cheerfully ships a wheel that is missing subpackages or data files, with no error. twine check won't catch it either — it validates metadata, not contents.

Always inspect the wheel and install it clean before publishing:

uv build
uv run python -m zipfile -l dist/*.whl # list every file the wheel contains
# ^ confirm ALL your subpackages (my_pkg/, my_pkg/sub/) and data files are there,
#   not just the top-level module.

The most common footgun is over-narrow file selection. This ships only server.py and silently drops the whole server/ package and data/:

# DON'T — over-narrow include drops everything else
[tool.hatch.build]
only-include = ["server.py"]

# DO — include the package (and any data dirs); let the backend walk it
[tool.hatch.build.targets.wheel]
packages = ["src/my_pkg"]

Name-collision footgun: never ship both a top-level module foo.py and a package directory foo/. The package shadows the module, so import foo resolves to the (often nearly empty) foo/__init__.py, and a console entry point foo = "foo:main" fails because that package has no main. Pick one — usually the package — and delete the other.

Then prove it from a clean install, not from the source tree:

uv venv /tmp/verify && uv pip install --python /tmp/verify/bin/python dist/*.whl
cd /tmp && /tmp/verify/bin/python -c "import my_pkg; my_pkg.submodule.real_func"
mycli --help                           # exercise each console script too

Run it from a directory other than the repo root — otherwise import my_pkg picks up the source tree on sys.path and "works" even when the wheel is empty. And assert on a real symbol (my_pkg.submodule.real_func), never just that the bare top-level name imports: import foo can succeed against a shadowing empty package and prove nothing. A CI smoke test that only does import foo; print("ok") is a false green — it passes whether or not the distributed package is usable.

Checklist

Before Release:
- [ ] pyproject.toml valid
- [ ] README.md informative
- [ ] LICENSE file exists
- [ ] Version set correctly
- [ ] twine check passes
- [ ] `uv run python -m zipfile -l dist/*.whl` shows every subpackage + data file
- [ ] Direct-reference dependencies pass a real backend build (not only install dry-run)
- [ ] No module/package name collision (no foo.py AND foo/)
- [ ] Installed the wheel into a clean venv and imported a real submodule
      symbol from a directory outside the repo (not just the top-level name)
- [ ] Each console script runs after a clean install

After Release:
- [ ] pip install works
- [ ] Import works
- [ ] GitHub release created

Learn More

This skill is based on the Distribution section of the Guide to Developing High-Quality Python Libraries by Will McGinnis. See these posts for deeper coverage: