A comprehensive reference for packaging, versioning, and publishing Python libraries.
pyproject.toml is the single source of truth for modern Python projects (PEP 621).
[build-system]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.build_meta"The [build-system] table tells pip and build which backend to use. Alternatives include flit-core, hatchling, and pdm-backend.
[project]
name = "my-package" # PyPI package name (use hyphens)
version = "1.0.0" # Follow semver
requires-python = ">=3.9" # Minimum Python version
dependencies = ["httpx"] # Runtime dependenciesKey fields:
name: Must be unique on PyPI. Use lowercase with hyphens.version: Semantic versioning (MAJOR.MINOR.PATCH).classifiers: Trove classifiers for PyPI search/filtering.optional-dependencies: Groups like [dev], [docs].Tools like ruff, mypy, and pytest read config from [tool.*] tables, keeping everything in one file.
Keep the version in one place. Options:
1. __init__.py: __version__ = "1.0.0" + version = attr: package.__version__
2. pyproject.toml: Canonical, read at build time
3. setuptools-scm: Derive version from git tags automatically
Some projects use YYYY.MM.DD format (e.g., pip, Ubuntu). Useful for projects without a stable API contract.
.tar.gz)MANIFEST.in.whl file (a zip with metadata)Rule of thumb: Always publish both sdist and wheel. Use python -m build which creates both by default.
src/ Layoutmy-project/
├── src/
│ └── my_package/
│ ├── __init__.py
│ └── core.py
├── tests/
│ └── test_core.py
└── pyproject.toml
Why src/ layout?
Add an empty py.typed marker file to your package root:
src/my_package/py.typed
Then include it in your package data:
[tool.setuptools.package-data]
my_package = ["py.typed"]This tells type checkers (mypy, pyright) that your package ships inline type annotations.
For large organizations splitting a namespace across multiple packages:
# Package A installs: myorg/auth/...
# Package B installs: myorg/billing/...
Use implicit namespace packages (PEP 420) — omit __init__.py in the shared namespace directory. Each sub-package is an independent distribution.
python -m build
python -m twine check dist/*
python -m twine upload dist/*Always test on TestPyPI before publishing to production:
python -m twine upload --repository testpypi dist/*
pip install --index-url https://test.pypi.org/simple/ my-package- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}Use Trusted Publishers (OIDC) for keyless authentication — no API tokens needed.
include LICENSE README.md
recursive-include src *.py py.typed
recursive-include tests *.py
global-exclude *.pyc __pycache__
Common mistakes:
py.typed in the sdist.git/, node_modules/, or CI configspip install dist/*.tar.gzrepos:
- repo: https://github.com/astral-sh/ruff-pre-commit
hooks:
- id: ruff
- id: ruff-formatstrategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
os: [ubuntu-latest, macos-latest, windows-latest]pyproject.toml (or __init__.py)make check)py.typed marker includedtwine check)pip install dist/*.tar.gzpip install dist/*.whlGet the full Python Packaging Guide and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.