agentleFS
wdm0006wdm0006/python-skills/skills/python/packaging/SKILL.md

packaging-python-libraries

Skill111 starsChanged 18 days agoMITView on GitHub

Packages and distributes Python libraries using modern pyproject.toml, build backends (setuptools, hatchling), PyPI publishing with trusted publishing, and wheel building. Use when packaging libraries for distribution, publishing to PyPI, or troubleshooting packaging issues.

  • Installs packages

The file

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 0 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Prefer trusted publishing from CI (no stored token) — see the CI section below.

    line 47
  2. Very specific:

    Do not use uv pip install --dry-run .

    line 104
  3. Very specific:

    Name-collision footgun: never ship both a top-level module foo.py and a package directory foo/.

    line 149
  4. Very specific:

    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 …

    line 163

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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

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:

  • ../project-setup/PYPROJECT.md - Complete annotated pyproject.toml (canonical)
  • CONDA.md - Conda / conda-forge packaging guide

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:

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.