Build backends
Write a complete pyproject.toml using hatchling and produce a wheel with python -m build.
- Write a pyproject.toml using hatchling as the build backend
- Declare project metadata including name, version, dependencies, and requires-python
- Register a console script entry point
- Build a wheel and sdist with python -m build
A pyproject.toml is the single configuration file that describes your package
to the build system, to pip, and to development tools. Getting this file right
is the difference between a package that installs cleanly everywhere and one that
only works on your laptop.
Choosing hatchling
PEP 517 decoupled the build frontend (python -m build) from the build
backend (the library that does the actual work). Several backends exist:
setuptools, flit, pdm-backend, and hatchling. This module uses hatchling
because it requires minimal configuration for pure-Python projects, has an active
maintenance team, and is the default backend for new Hatch projects.
A complete pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-tool"
version = "0.1.0"
description = "A CLI utility for processing text files."
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
authors = [
{ name = "Your Name", email = "you@example.com" },
]
keywords = ["cli", "text-processing"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Environment :: Console",
]
dependencies = [
"click>=8.1",
"rich>=13.0",
]
[project.scripts]
my-tool = "my_tool.cli:main"
[project.urls]
Homepage = "https://github.com/you/my-tool"Each section does specific work:
[build-system]tellspipandpython -m buildwhich backend to call.[project]is the canonical metadata — this is what PyPI displays and whatpip showprints.requires-pythonis enforced at install time;pipwill refuse to install on an older interpreter.dependenciesare pinned with>=lower bounds, not==— let users decide the upper bound unless you have a known incompatibility.[project.scripts]maps themy-toolcommand to themainfunction inmy_tool/cli.py. This is what creates the executable when the package is installed.
Project layout
Hatchling expects your source code in either src/my_tool/ (the src layout) or
my_tool/ at the root. The src layout is preferred because it prevents accidental
imports of the un-installed package during testing:
my-tool/
├── pyproject.toml
├── README.md
├── src/
│ └── my_tool/
│ ├── __init__.py
│ └── cli.py
└── tests/
└── test_cli.pyTell hatchling to look in src/ by adding one line:
[tool.hatch.build.targets.wheel]
packages = ["src/my_tool"]Building
Install the build frontend once:
pip install buildThen build both artifacts from the project root:
python -m buildThis produces two files in dist/:
dist/
├── my_tool-0.1.0-py3-none-any.whl
└── my_tool-0.1.0.tar.gzThe wheel is what pip will prefer when installing. The sdist is the fallback.
Run twine check dist/* immediately after building. It validates your metadata
against PyPI's requirements and catches problems before upload — missing
descriptions, malformed classifiers, and similar issues that would cause a
rejected upload.
Where to go next
Next: versioning — applying SemVer, exposing __version__ from
importlib.metadata, and understanding pre-release suffixes.