# Publishing Python Packages

Publish packages to PyPI with modern tooling.

## pyproject.toml for Publishing

```toml
[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",
]

# Dev tooling → dependency-groups (PEP 735); not shipped with the package.
[dependency-groups]
dev = [
    "pytest>=8.0",
    "pytest-cov>=5.0",
    "ruff>=0.4",
    "mypy>=1.10",
]

[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

```bash
# Build sdist + wheel — uv has a native builder, no separate `build` install
uv build

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

# Verify + upload with twine via uvx (ephemeral, nothing installed globally)
uvx twine check dist/*

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

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

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

## Version Management

### Option 1: Manual version

```toml
[project]
version = "0.1.0"
```

### Option 2: Dynamic from __init__.py

```toml
[project]
dynamic = ["version"]

[tool.hatch.version]
path = "src/my_package/__init__.py"
```

```python
# src/my_package/__init__.py
__version__ = "0.1.0"
```

### Option 3: Git tags with hatch-vcs

```toml
[project]
dynamic = ["version"]

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

[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
```

```bash
# 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)

```markdown
# 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

```yaml
# .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

      - name: Install uv
        uses: astral-sh/setup-uv@v5

      - name: Build package
        run: uv build

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

## PyPI Configuration

### ~/.pypirc (for twine)

```ini
[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 |
|---------|---------|
| `uv build` | Build wheel and sdist (native, no `build` install) |
| `uvx twine check dist/*` | Verify package |
| `uvx twine upload dist/*` | Upload to PyPI |
| `uvx 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

```markdown
- [ ] 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
```
