Packaging & Publishing
Build distributable Python packages with pyproject.toml, setuptools, and publish to PyPI. Learn project structure, versioning, and release workflows.
Packaging turns your Python code into installable, shareable modules that others can pip install.
Project Structure
A standard layout:
myproject/
├── pyproject.toml
├── README.md
├── LICENSE
├── src/
│ └── mypackage/
│ ├── __init__.py
│ └── core.py
└── tests/
└── test_core.py
The src/ layout prevents accidentally importing from the local directory instead of the installed package.
pyproject.toml
Modern Python projects use pyproject.toml as the single configuration file:
[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "mypackage"
version = "0.1.0"
description = "A useful Python package"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
authors = [{name = "Your Name", email = "[email protected]"}]
dependencies = [
"requests>=2.28",
]
[project.optional-dependencies]
dev = ["pytest>=7.0", "mypy>=1.0"]
[project.scripts]
mycli = "mypackage.cli:main"
[tool.setuptools.packages.find]
where = ["src"]
Building the Package
pip install build
python -m build
This creates dist/mypackage-0.1.0.tar.gz and dist/mypackage-0.1.0-py3-none-any.whl.
Installing Locally
# Editable install (changes reflect immediately)
pip install -e ".[dev]"
# From a built wheel
pip install dist/mypackage-0.1.0-py3-none-any.whl
Publishing to PyPI
# Test first
twine upload --repository testpypi dist/*
# Production
twine upload dist/*
Use API tokens instead of passwords for authentication.
Project Metadata and Classifiers
Help users discover your package on PyPI with classifiers:
[project]
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",
"Topic :: Software Development :: Libraries",
]
keywords = ["utilities", "cli", "automation"]
CHANGELOG and Version Bumping
Maintain a CHANGELOG.md following Keep a Changelog:
## [0.2.0] - 2026-06-13
### Added
- New `fetch_all()` function for batch requests
### Fixed
- Handle empty response bodies
Bump version in pyproject.toml before each release. Automate with bump2version or Poetry’s version command.
Including Non-Python Files
Package data files (templates, config defaults) via pyproject.toml:
[tool.setuptools.package-data]
mypackage = ["templates/*.html", "data/*.json"]
Or use MANIFEST.in for setuptools:
include LICENSE
include README.md
recursive-include src/mypackage/templates *.html
CI Publishing with GitHub Actions
# .github/workflows/publish.yml
name: Publish to PyPI
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install build twine
- run: python -m build
- run: twine upload dist/*
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
Always test on TestPyPI before your first production release.
Semantic Versioning
Follow SemVer: MAJOR.MINOR.PATCH
- MAJOR — breaking API changes
- MINOR — new features, backward compatible
- PATCH — bug fixes, backward compatible
Entry Points and CLI Tools
Define command-line scripts in pyproject.toml:
[project.scripts]
mytool = "mypackage.cli:main"
# src/mypackage/cli.py
def main():
print("Hello from mytool!")
After pip install, users can run mytool from anywhere.
Checklist Before Publishing
- Tests pass (
pytest) - Type checks pass (
mypy) - README has install and usage instructions
- LICENSE file included
- Version bumped in
pyproject.toml - No secrets or credentials in the code
- Tested on TestPyPI first
Publishing packages is how you contribute reusable tools to the Python ecosystem.