Simple packaging¶
Python packages can now use a modern build system instead of the classic but
verbose setuptools and setup.py. The one you select doesn’t really matter that
much; they all use a standard configuration language introduced in
PEP 621. The PyPA’s Flit is a great option. scikit-build-core and
meson-python are being developed to support this sort of configuration,
enabling binary extension packages to benefit too. These PEP 621 tools
currently include Hatch, PDM, Flit, Setuptools, Poetry 2.0,
and compiled backends (see the next page).
Also see the Python packaging guide, especially the Python packaging tutorial.
pyproject.toml: build-system¶
PY001 Packages must have a pyproject.toml file PP001 that
selects the backend:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"[build-system]
requires = ["uv_build>=0.7.19"]
build-backend = "uv_build"[build-system]
requires = ["flit_core>=3.12"]
build-backend = "flit_core.buildapi"[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"pyproject.toml: project table¶
The metadata is specified in a standards-based format:
[project]
name = "package"
version = "0.1.0"
authors = [
{ name = "My Name", email = "me@email.com" },
]
description = "A great package."
readme = "README.md"
license = "BSD-3-Clause"
license-files = ["LICENSE"]
requires-python = ">=3.10"
classifiers = [
"Development Status :: 1 - Planning",
"Intended Audience :: Science/Research",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Scientific/Engineering",
"Typing :: Typed",
]
dependencies = []
[project.urls]
Homepage = "https://github.com/org/package"
"Bug Tracker" = "https://github.com/org/package/issues"
Discussions = "https://github.com/org/package/discussions"
Changelog = "https://github.com/org/package/releases"In this example, "package" is the name of the thing you are working on. You
can read more about each field, and all allowed fields, in
packagingname and version fields are strictly required. Note that “Homepage” is
special, and replaces the old url setting.
If you use the above configuration, you need README.md and LICENSE files,
since they are explicitly specified.
License¶
The license can be done one of two ways.
The modern way is to use the license field and an SPDX identifier
expression. You can specify a list of files globs in license-files. You
need hatchling>=1.26, flit-core>=1.11 (1.12 for complex license statements),
pdm-backend>=2.4, setuptools>=77, meson-python>=0.18, maturin>=1.9.2,
poetry-core>=2.2, or scikit-build-core>=0.12 to support this. You can also
specify license-files as a list with globs for license files. If you don’t,
most backends will discover common license file names by default.
The classic convention uses one or more Trove Classifiers to specify the
license. There also was a license.file field, required by meson-python, but
other tools often did the wrong thing (such as load the entire file into the
metadata’s free-form one line text field that was intended to describe
deviations from the classifier license(s)).
classifiers = [
"License :: OSI Approved :: BSD License",
]You should not include the License :: classifiers if you use the license
field PP007.
Extras¶
Sometimes you want to ship a package with optional dependencies. For example,
you might have extra requirements that are only needed for running a CLI, or for
plotting. Users must opt-in to get these dependencies by adding them to the
package or wheel name when installing, like package[cli,mpl].
Here is an example of a simple extras:
[project.optional-dependencies]
cli = [
"click",
]
mpl = [
"matplotlib >=2.0",
]Self dependencies can be used by using the name of the package, such as
all = ["package[cli,mpl]"], (requires Pip 21.2+).
Command line¶
If you want to ship an “app” that a user can run from the command line, you need
to add a script entry point. The form is:
[project.scripts]
cliapp = "package.__main__:main"The format is command line app name as the key, and the value is the path to the
function, followed by a colon, then the function to call. If you use
__main__.py as the file, then python -m followed by the module will also
work to call the app (__name__ will be "__main__" in that case).
Development dependencies¶
The proper way to specify dependencies exclusively used for development tasks
(such as pytest, ruff, packages for generating documentation, etc.) is to
use dependency-groups. Dependency-groups are recommended over requirement files
because they are formally standardized (i.e. they will be more portable going
forward) and they are more composable. In contrast with extras,
dependency-groups are not available when installing your package via PyPI, but
they are available for local installation (and can be installed separately from
your package); the dev group is even installed, by default, when using uv’s
high level commands like uv run and uv sync. PP006 Here is an
example:
[dependency-groups]
test = [
"pytest >=9",
"pytest-cov >=7",
]
dev = [
{ include-group = "test" },
]
docs = [
"sphinx>=7.0",
"myst_parser>=0.13",
"sphinx_copybutton",
"sphinx_autodoc_typehints",
"furo>=2023.08.17",
]You can include one dependency group in another. Most tools allow you to install
groups using --group, like pip (25.1+), uv pip, and the high level uv
interface. It’s possible to install a package’s dependency group without
installing the package itself, but usually you’ll want to instruct your tool to
install both (the high level uv interface does this automatically). Nox, Tox,
and cibuildwheel all support groups, too. The dependency-groups package also
provides tools to get the dependencies.
For requires-python, you should specify the minimum you require, and you
should not put an upper cap on it PY004, as this field is used to
back-solve for old package versions that pass this check, allowing you to safely
drop Python versions.
Package structure¶
All packages should have a src folder, with the package code residing inside
it, such as src/<package>/. This may seem like extra hassle; after all, you
can type “python” in the main directory and avoid installing it if you don’t
have a src folder! However, this is a bad practice, and it causes several
common bugs, such as running pytest and getting the local version instead of
the installed version - this obviously tends to break if you build parts of the
library or if you access package metadata.
This sadly is not part of the standard metadata in [project], so it depends on
what backend you you use. Hatchling, Flit, PDM, and setuptools use automatic
detection.
If you don’t match your package name and import name (which you should except for very special cases), you will likely need extra configuration here.
You should have a README PY002 and a LICENSE PY003 file.
You should have a docs/ folder PY004. You should have a /tests
folder PY005 (recommended) and/or a src/<package>/tests folder.
Versioning¶
You can specify the version manually (as shown in the example), but the backends usually provide some automatic features to help you avoid this. Flit will pull this from a file if you ask it to. Hatchling and PDM can be instructed to look in a file or use git.
You will always need to specify that the version will be supplied dynamically with:
dynamic = ["version"]Then you’ll configure your backend to compute the version.
Hatchling dynamic versioning
You can tell hatchling to get the version from VCS. Add hatch-vcs to your
build-backend.requires, then add the following configuration:
[tool.hatch]
version.source = "vcs"
build.hooks.vcs.version-file = "src/<package>/version.py"Or you can tell it to look for it in a file (see docs for arbitrary regex’s):
[tool.hatch]
version.path = "src/<package>/__init__.py"(replace <package> with the package path).
You should also add these two files:
.git_archival.txt:
node: $Format:%H$
node-date: $Format:%cI$
describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$And .gitattributes (or add this line if you are already using this file):
.git_archival.txt export-substThis will allow git archives (including the ones generated from GitHub) to also support versioning.
Including/excluding files in the SDist¶
This is tool specific.
Hatchling info here. Hatchling uses your VCS ignore file by default, so make sure it is accurate (which is a good idea anyway).
Flit info here. Flit requires manual inclusion/exclusion in many cases, like using a dirty working directory.
Setuptools still uses
MANIFEST.in.