publishing.md 5.0 KB

Publishing Python Packages

Publish packages to PyPI with modern tooling.

pyproject.toml for Publishing

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "my-package"
version = "0.1.0"
description = "My awesome package"
readme = "README.md"
license = {file = "LICENSE"}
authors = [
    {name = "Your Name", email = "you@example.com"},
]
keywords = ["keyword1", "keyword2"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
]
requires-python = ">=3.10"
dependencies = [
    "requests>=2.28",
    "pydantic>=2.0",
]

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

[project.urls]
Homepage = "https://github.com/username/my-package"
Documentation = "https://my-package.readthedocs.io"
Repository = "https://github.com/username/my-package"
Changelog = "https://github.com/username/my-package/blob/main/CHANGELOG.md"

[project.scripts]
my-command = "my_package.cli:main"

[project.entry-points."my_package.plugins"]
plugin1 = "my_package.plugins:Plugin1"

Build and Upload

# Install build tools
uv pip install build twine

# Build package
python -m build

# Check build artifacts
ls dist/
# my_package-0.1.0-py3-none-any.whl
# my_package-0.1.0.tar.gz

# Check distribution
twine check dist/*

# Upload to TestPyPI first
twine upload --repository testpypi dist/*

# Test installation from TestPyPI
uv pip install --index-url https://test.pypi.org/simple/ my-package

# Upload to PyPI (production)
twine upload dist/*

Version Management

Option 1: Manual version

[project]
version = "0.1.0"

Option 2: Dynamic from init.py

[project]
dynamic = ["version"]

[tool.hatch.version]
path = "src/my_package/__init__.py"
# src/my_package/__init__.py
__version__ = "0.1.0"

Option 3: Git tags with hatch-vcs

[project]
dynamic = ["version"]

[tool.hatch.version]
source = "vcs"

[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
# Create version tag
git tag -a v0.1.0 -m "Release 0.1.0"
git push origin v0.1.0

Semantic Versioning

MAJOR.MINOR.PATCH

Examples:
0.1.0 - Initial development
0.2.0 - New features (minor)
0.2.1 - Bug fixes (patch)
1.0.0 - First stable release
1.1.0 - New features, backwards compatible
2.0.0 - Breaking changes

Changelog (CHANGELOG.md)

# Changelog

All notable changes to this project will be documented in this file.

## [Unreleased]

### Added
- New feature X

### Changed
- Updated dependency Y

### Fixed
- Bug in Z

## [0.1.0] - 2024-01-15

### Added
- Initial release
- Core functionality

[Unreleased]: https://github.com/user/repo/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/user/repo/releases/tag/v0.1.0

GitHub Actions CI/CD

# .github/workflows/publish.yml
name: Publish to PyPI

on:
  release:
    types: [published]

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      id-token: write  # For trusted publishing

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install build tools
        run: pip install build

      - name: Build package
        run: python -m build

      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        # Uses trusted publishing - no token needed

PyPI Configuration

~/.pypirc (for twine)

[pypi]
username = __token__
password = pypi-xxxx...

[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-xxxx...

Trusted Publishing (Recommended)

  1. Go to PyPI → Your project → Publishing
  2. Add new trusted publisher
  3. Set GitHub repo and workflow file
  4. No API token needed in CI

Source Distribution Layout

my-package/
├── pyproject.toml
├── README.md
├── LICENSE
├── CHANGELOG.md
├── src/
│   └── my_package/
│       ├── __init__.py
│       └── core.py
└── tests/
    └── test_core.py

Quick Reference

Command Purpose
python -m build Build wheel and sdist
twine check dist/* Verify package
twine upload dist/* Upload to PyPI
twine upload --repository testpypi dist/* Upload to TestPyPI
Version When
0.x.x Initial development
x.0.0 Breaking changes
x.x.0 New features
x.x.x Bug fixes

Checklist Before Publishing

- [ ] Version updated in pyproject.toml
- [ ] CHANGELOG.md updated
- [ ] README.md current
- [ ] All tests passing
- [ ] Type checks passing
- [ ] Build succeeds locally
- [ ] TestPyPI upload works
- [ ] Installation from TestPyPI works