All skills
github avatar

/python-pypi-package-builder

@49fd3f3 official
by githubgithub/awesome-copilot40k stars
5,040

End-to-end skill for building, testing, linting, versioning, and publishing a production-grade Python library to PyPI. Covers all four build backends (setuptools+setuptools_scm, hatchling, flit, poetry), PEP 440 versioning, semantic versioning, dynamic git-tag versioning, OOP/SOLID design, type hints (PEP 484/526/544/561), Trusted Publishing (OIDC), and the full PyPA packaging flow. Use for: creating Python packages, pip-installable SDKs, CLI tools, framework plugins, pyproject.toml setup, py.typed, setuptools_scm, semver, mypy, pre-commit, GitHub Actions CI/CD, or PyPI publishing.

Use this Skill: https://skilld.dev/gh/github/awesome-copilot/python-pypi-package-builder

This session only. Nothing lands on disk.

referencespyproject-toml.md

≈3.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

pyproject.toml, Backends, Versioning, and Typed Package

Table of Contents

  1. Complete pyproject.toml — setuptools + setuptools_scm
  2. hatchling (modern, zero-config)
  3. flit (minimal, version from __version__)
  4. poetry (integrated dep manager)
  5. Versioning Strategy — PEP 440, semver, dep specifiers
  6. setuptools_scm — dynamic version from git tags
  7. setup.py shim for legacy editable installs
  8. PEP 561 typed package (py.typed)

1. Complete pyproject.toml

setuptools + setuptools_scm (recommended for git-tag versioning)

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

[project]
name = "your-package"
dynamic = ["version"]           # Version comes from git tags via setuptools_scm
description = "<your description> — <key feature 1>, <key feature 2>"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"                # PEP 639 SPDX expression (string, not {text = "MIT"})
license-files = ["LICENSE"]
authors = [
    {name = "Your Name", email = "you@example.com"},
]
maintainers = [
    {name = "Your Name", email = "you@example.com"},
]
keywords = [
    "python",
    # Add 10-15 specific keywords that describe your library — they affect PyPI discoverability
]
classifiers = [
    "Development Status :: 3 - Alpha",          # Change to 5 at stable release
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
    "Topic :: Software Development :: Libraries :: Python Modules",
    "Typing :: Typed",          # Add this when shipping py.typed
]
dependencies = [
    # List your runtime dependencies here. Keep them minimal.
    # Example: "httpx>=0.24", "pydantic>=2.0"
    # Leave empty if your library has no required runtime deps.
]

[project.optional-dependencies]
redis = [
    "redis>=4.2",               # Optional heavy backend
]
dev = [
    "pytest>=7.0",
    "pytest-asyncio>=0.21",
    "httpx>=0.24",
    "pytest-cov>=4.0",
    "ruff>=0.4",
    "black>=24.0",
    "isort>=5.13",
    "mypy>=1.0",
    "pre-commit>=3.0",
    "build",
    "twine",
]

[project.urls]
Homepage      = "https://github.com/yourusername/your-package"
Documentation = "https://github.com/yourusername/your-package#readme"
Repository    = "https://github.com/yourusername/your-package"
"Bug Tracker" = "https://github.com/yourusername/your-package/issues"
Changelog     = "https://github.com/yourusername/your-package/blob/master/CHANGELOG.md"

# --- Setuptools configuration ---
[tool.setuptools.packages.find]
include = ["your_package*"]   # flat layout
# For src/ layout, use:
# where = ["src"]

[tool.setuptools.package-data]
your_package = ["py.typed"]  # Ship the py.typed marker in the wheel

# --- setuptools_scm: version from git tags ---
[tool.setuptools_scm]
version_scheme = "post-release"
local_scheme   = "no-local-version"  # Prevents +local suffix breaking PyPI uploads

# --- Ruff (linting) ---
[tool.ruff]
target-version = "py310"
line-length    = 100

[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "B", "SIM", "C4", "PTH", "RUF"]
ignore = ["E501"]   # Line length enforced by formatter

[tool.ruff.lint.per-file-ignores]
"tests/*"   = ["S101", "ANN"]    # Allow assert and missing annotations in tests
"scripts/*" = ["T201"]           # Allow print in scripts

[tool.ruff.format]
quote-style = "double"

# --- Black (formatting) ---
[tool.black]
line-length    = 100
target-version = ["py310", "py311", "py312", "py313"]

# --- isort (import sorting) ---
[tool.isort]
profile     = "black"
line_length = 100

# --- mypy (static type checking) ---
[tool.mypy]
python_version         = "3.10"
warn_return_any        = true
warn_unused_configs    = true
warn_unused_ignores    = true
disallow_untyped_defs  = true
disallow_any_generics  = true
ignore_missing_imports = true
strict                 = false     # Set true for maximum strictness

[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false     # Relaxed in tests

# --- pytest ---
[tool.pytest.ini_options]
asyncio_mode  = "auto"
testpaths     = ["tests"]
pythonpath    = ["."]          # For flat layout; remove for src/
python_files  = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
addopts       = "-v --tb=short --cov=your_package --cov-report=term-missing"

# --- Coverage ---
[tool.coverage.run]
source = ["your_package"]
omit   = ["tests/*"]

[tool.coverage.report]
fail_under   = 80
show_missing = true
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise NotImplementedError",
    "if TYPE_CHECKING:",
    "@abstractmethod",
]

2. hatchling (Modern, Zero-Config)

Best for new pure-Python projects that don't need C extensions. No setup.py needed. Use hatch-vcs for git-tag versioning, or omit it for manual version bumps.

[build-system]
requires = ["hatchling", "hatch-vcs"]     # hatch-vcs for git-tag versioning
build-backend = "hatchling.build"

[project]
name = "your-package"
dynamic = ["version"]            # Remove and add version = "1.0.0" for manual versioning
description = "One-line description"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
license-files = ["LICENSE"]
authors = [{name = "Your Name", email = "you@example.com"}]
keywords = ["python"]
classifiers = [
    "Development Status :: 3 - Alpha",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
    "Programming Language :: Python :: 3",
    "Typing :: Typed",
]
dependencies = []

[project.optional-dependencies]
dev = ["pytest>=8.0", "pytest-cov>=5.0", "ruff>=0.6", "mypy>=1.10"]

[project.urls]
Homepage  = "https://github.com/yourusername/your-package"
Changelog = "https://github.com/yourusername/your-package/blob/master/CHANGELOG.md"

# --- Hatchling build config ---
[tool.hatch.build.targets.wheel]
packages = ["src/your_package"]    # src/ layout
# packages = ["your_package"]      # ← flat layout

[tool.hatch.version]
source = "vcs"                     # git-tag versioning via hatch-vcs

[tool.hatch.version.raw-options]
local_scheme = "no-local-version"

# ruff, mypy, pytest, coverage sections — same as setuptools template above

3. flit (Minimal, Version from __version__)

Best for very simple, single-module packages. Zero config. Version is read directly from your_package/__init__.py. Always requires a static string for __version__.

[build-system]
requires = ["flit_core>=3.9"]
build-backend = "flit_core.buildapi"

[project]
name = "your-package"
dynamic = ["version", "description"]  # Read from __init__.py __version__ and docstring
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [{name = "Your Name", email = "you@example.com"}]
classifiers = [
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Typing :: Typed",
]
dependencies = []

[project.urls]
Homepage = "https://github.com/yourusername/your-package"

# flit reads __version__ from your_package/__init__.py automatically.
# Ensure __init__.py has: __version__ = "1.0.0"  (static string — flit does NOT support
# importlib.metadata for dynamic version discovery)

4. poetry (Integrated Dependency + Build Manager)

Best for teams that want a single tool to manage deps, build, and publish. Poetry v2+ supports the standard [project] table.

[build-system]
requires = ["poetry-core>=2.0"]
build-backend = "poetry.core.masonry.api"

[project]
name = "your-package"
version = "1.0.0"
description = "One-line description"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [{name = "Your Name", email = "you@example.com"}]
classifiers = [
    "Programming Language :: Python :: 3",
    "Typing :: Typed",
]
dependencies = []   # poetry v2+ uses standard [project] table

[project.optional-dependencies]
dev = ["pytest>=8.0", "ruff>=0.6", "mypy>=1.10"]

# Optional: use [tool.poetry] only for poetry-specific features
[tool.poetry.group.dev.dependencies]
# Poetry-specific group syntax (alternative to [project.optional-dependencies])
pytest = ">=8.0"

5. Versioning Strategy

PEP 440 — The Standard

Canonical form:  N[.N]+[{a|b|rc}N][.postN][.devN]

Examples:
  1.0.0          Stable release
  1.0.0a1        Alpha (pre-release)
  1.0.0b2        Beta
  1.0.0rc1       Release candidate
  1.0.0.post1    Post-release (e.g., packaging fix only — no code change)
  1.0.0.dev1     Development snapshot (NOT for PyPI)

Semantic Versioning (SemVer) — use this for every library

MAJOR.MINOR.PATCH

MAJOR: Breaking API change (remove/rename public function/class/arg)
MINOR: New feature, fully backward-compatible
PATCH: Bug fix, no API change
Change What bumps Example
Remove / rename a public function MAJOR 1.2.3 → 2.0.0
Add new public function MINOR 1.2.3 → 1.3.0
Bug fix, no API change PATCH 1.2.3 → 1.2.4
New pre-release suffix 2.0.0a1, 2.0.0rc1

Version in code — read from package metadata

# your_package/__init__.py
from importlib.metadata import version, PackageNotFoundError

try:
    __version__ = version("your-package")
except PackageNotFoundError:
    __version__ = "0.0.0-dev"    # Fallback for uninstalled dev checkouts

Never hardcode __version__ = "1.0.0" when using setuptools_scm — it goes stale after the first git tag. Use importlib.metadata always.

Version specifier best practices for dependencies

# In [project] dependencies — for a LIBRARY:
"httpx>=0.24"            # Minimum version — PREFERRED for libraries
"httpx>=0.24,<1.0"       # Upper bound only when a known breaking change exists

# ONLY for applications (never for libraries):
"httpx==0.27.0"          # Pin exactly — breaks dep resolution in libraries

# NEVER do this in a library:
# "httpx~=0.24.0"        # Compatible release operator — too tight
# "httpx==0.27.*"        # Wildcard pin — fragile

6. Dynamic Versioning with setuptools_scm

setuptools_scm reads your git tags and sets the package version automatically — no more manually editing version strings before each release.

How it works

git tag v1.0.0        →  package version = 1.0.0
git tag v1.1.0        →  package version = 1.1.0
(commits after tag)   →  version = 1.1.0.post1+g<hash>  (stripped for PyPI)

local_scheme = "no-local-version" strips the +g<hash> suffix so PyPI uploads never fail with a "local version label not allowed" error.

Access version at runtime

# your_package/__init__.py
from importlib.metadata import version, PackageNotFoundError

try:
    __version__ = version("your-package")
except PackageNotFoundError:
    __version__ = "0.0.0-dev"  # Fallback for uninstalled dev checkouts

Never hardcode __version__ = "1.0.0" when using setuptools_scm — it will go stale after the first tag.

Full release flow (this is it — nothing else needed)

git tag v1.2.0
git push origin master --tags
# GitHub Actions publish.yml triggers automatically

7. setup.py Shim

Some older tools and IDEs still expect a setup.py. Keep it as a three-line shim — all real configuration stays in pyproject.toml.

# setup.py — thin shim only. All config lives in pyproject.toml.
from setuptools import setup

setup()

Never duplicate name, version, dependencies, or any other metadata from pyproject.toml into setup.py. If you copy anything there it will eventually drift and cause confusing conflicts.


8. Typed Package (PEP 561)

A properly declared typed package means mypy, pyright, and IDEs automatically pick up your type hints without any extra configuration from your users.

Step 1: Create the marker file

# The file must exist; its content doesn't matter — its presence is the signal.
touch your_package/py.typed

Step 2: Include it in the wheel

Already in the template above:

[tool.setuptools.package-data]
your_package = ["py.typed"]

Step 3: Add the PyPI classifier

classifiers = [
    ...
    "Typing :: Typed",
]

Step 4: Type-annotate all public functions

# Good — fully typed
def process(
    self,
    data: dict[str, object],
    *,
    timeout: int = 30,
) -> dict[str, object]:
    ...

# Bad — mypy will flag this, and IDEs give no completions to users
def process(self, data, timeout=30):
    ...

Step 5: Verify py.typed ships in the wheel

python -m build
unzip -l dist/your_package-*.whl | grep py.typed
# Must show: your_package/py.typed

If it's missing, check your [tool.setuptools.package-data] config.

Source: SKILL.md on GitHub

No alerts10d4 checks · Risk SAFE
  • Gen Agent Trust Hub10d

    This skill provides a comprehensive and secure framework for building, testing, and publishing Python packages following industry best practices. It incorporates secure publishing via OpenID Connect (OIDC), uses modern and efficient tooling like Ruff, and provides a local scaffolding script that follows standard templating procedures. All external references and dependencies target well-known and trusted organizations within the Python and GitHub ecosystems.

  • Socket10d

    No alerts

  • Snyk10d

    Risk: LOW · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 49fd3f3. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 6 months ago
  • Python
  • pypi
  • packaging
  • setuptools
  • hatchling
  • flit
  • poetry
  • versioning
  • ci-cd
  • github-actions

README badge

README badge for github/awesome-copilot/python-pypi-package-builder

Covers the full PyPI publishing pipeline for Python packages: build backend selection (setuptools+setuptools_scm, hatchling, flit, poetry), project structure (src/ vs flat), type hints, testing, versioning (PEP 440 / semver / git-tag), and GitHub Actions CI/CD with Trusted Publishing. Targets teams building pip-installable libraries, SDKs, CLI tools, or framework plugins.

Generated from the current SKILL.md.

Which build backend should I use?
Use setuptools + setuptools_scm if you want version derived from git tags. Use hatchling or flit for pure-Python projects without git-tag versioning. Use poetry v2+ if you want an all-in-one tool. Use setuptools only if you have C/Cython extensions.
Does this skill support publishing with Trusted Publishing (OIDC)?
Yes. The skill covers Trusted Publishing setup in the CI/publishing workflow references and includes GitHub Actions templates for OIDC-based PyPI publishing.
What folder structure should I use for a new package?
Use src/ layout as the default for new projects — it prevents accidental imports of uninstalled code and is PyPA-recommended. Use flat layout only for single-purpose packages with 1–4 modules.
Does this cover type hints and mypy configuration?
Yes. The skill includes PEP 484/526/544/561 type hint patterns, py.typed marker file setup, and complete mypy configuration in the testing and quality reference.
Can I use this skill for CLI tools, SDKs, and plugins?
Yes. The skill covers all package types including utility libraries, API clients/SDKs, CLI tools, framework plugins, and data processing libraries, with patterns and entry-point configuration for each.

Generated from the current SKILL.md. These answers refresh after source changes.