Package Distribution and PyPI Publishing
Build, package, and distribute Python projects with pyproject.toml, setuptools, wheels, versioning, and PyPI publishing
Package Distribution and PyPI Publishing
Why Package Your Code?
Packaging enables installation via pip, declares dependencies, provides entry points, and makes your code reusable across projects.
pyproject.toml (Modern Standard)
pyproject.toml is the modern Python packaging standard (PEP 517/518/621). It replaces setup.py and setup.cfg.
[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-awesome-lib"
version = "0.1.0"
description = "A short description of my library"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
keywords = ["python", "example", "tutorial"]
authors = [
{name = "Your Name", email = "you@example.com"},
]
classifiers = [
"Development Status :: 3 - Alpha",
"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 :: Python Modules",
]
dependencies = [
"requests>=2.28",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
"black>=23.0",
"ruff>=0.1",
"mypy>=1.0",
]
test = ["pytest>=7.0", "httpx>=0.24"]
docs = ["mkdocs>=1.4", "mkdocstrings"]
[project.urls]
Homepage = "https://github.com/you/my-awesome-lib"
Documentation = "https://my-awesome-lib.readthedocs.io"
Repository = "https://github.com/you/my-awesome-lib"
Issues = "https://github.com/you/my-awesome-lib/issues"
[project.scripts]
my-cli = "my_awesome_lib.cli:main"
[project.gui-scripts]
my-gui = "my_awesome_lib.gui:launch"
[tool.setuptools.packages.find]
where = ["src"]
include = ["my_awesome_lib*"]
exclude = ["tests*", "docs*"]The [project.scripts] section creates console entry points. When users pip install your package, these become executable commands on their PATH.
Project Structure
my-awesome-lib/
βββ pyproject.toml
βββ README.md
βββ LICENSE
βββ CHANGELOG.md
βββ src/
β βββ my_awesome_lib/
β βββ __init__.py
β βββ cli.py
β βββ core.py
β βββ utils.py
βββ tests/
β βββ __init__.py
β βββ test_core.py
β βββ test_cli.py
βββ docs/
βββ index.md
βββ api.md
Using src/ Layout
The src/ layout prevents import confusion during development and testing.
# src/my_awesome_lib/core.py
def add(a, b):
"""Add two numbers."""
return a + b
# src/my_awesome_lib/cli.py
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("numbers", nargs=2, type=float)
args = parser.parse_args()
result = add(args.numbers[0], args.numbers[1])
print(f"Result: {result}")
# src/my_awesome_lib/__init__.py
from .core import addBuilding Wheels
# Install build tools
pip install build twine
# Build source distribution and wheel
python -m build
# Check the built wheel
ls dist/
# my_awesome_lib-0.1.0.tar.gz
# my_awesome_lib-0.1.0-py3-none-any.whlWheels (.whl) are the preferred distribution format. They install faster than source distributions because they skip the build step.
Uploading to PyPI
# Upload to TestPyPI first
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
# Upload to production PyPI
twine upload dist/*
# Install from TestPyPI
pip install --index-url https://test.pypi.org/simple/ my-awesome-libUsing ~/.pypirc
[distutils]
index-servers =
pypi
testpypi
[pypi]
username = __token__
password = pypi-xxxxx...
[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-xxxxx...Never commit your PyPI token or password. Use environment variables (TWINE_USERNAME, TWINE_PASSWORD) or keyring in CI/CD.
Versioning
Semantic Versioning (SemVer)
MAJOR.MINOR.PATCH
MAJOR: Incompatible API changes
MINOR: Backward-compatible new features
PATCH: Backward-compatible bug fixes
# __version__.py β single source of truth
__version__ = "0.1.0"Dynamic Versioning with setuptools-scm
[build-system]
requires = ["setuptools>=68.0", "wheel", "setuptools-scm>=8.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my-awesome-lib"
dynamic = ["version"]
[tool.setuptools_scm]
version_scheme = "post-release"Version is derived from git tags (git tag v0.1.0).
Publishing Workflow (CI/CD)
# .github/workflows/publish.yml
name: Publish to PyPI
on:
release:
types: [published]
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- 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_TOKEN }}Use Trusted Publishing (OIDC) with PyPI for the most secure CI/CD setup. It requires no stored tokens at all.
Manifest Files
# MANIFEST.in β include extra files in source distribution
include README.md
include LICENSE
include CHANGELOG.md
recursive-include src/my_awesome_lib/data *Complete Package Checklist
Real-World: Publishing a CLI Tool
# src/mycalc/cli.py
import argparse
from .core import calculate
def main():
parser = argparse.ArgumentParser(description="A simple calculator")
parser.add_argument("expression", help="Math expression to evaluate")
args = parser.parse_args()
result = calculate(args.expression)
print(f"= {result}")
if __name__ == "__main__":
main()[project.scripts]
mycalc = "mycalc.cli:main"After pip install mycalc:
mycalc "2 + 2"
# = 4Practice Questions
- What is the
pyproject.tomlfile and why is it preferred oversetup.py? - Create a
pyproject.tomlfor a package calledtextutilswith dependencies onclickandpyyaml. - What is the difference between a source distribution (
.tar.gz) and a wheel (.whl)? When would you use each? - Write a GitHub Actions workflow that publishes a package to PyPI when a release is created.
- How does
setuptools-scmderive the package version from git? What are the benefits? - What is the
src/layout and why is it recommended for Python packages? - Build a simple CLI tool with
pyproject.tomlentry points and publish it to TestPyPI. - How do you handle optional dependencies in
pyproject.toml? Give an example withdevandtestextras. - What is Trusted Publishing (OIDC) on PyPI and how does it improve security?
- Create a complete project structure for a package named
csvprocthat processes CSV files with proper testing, documentation, and packaging.