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