Simple packaging (original) (raw)
Table of contents
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.
Classic files
These systems do not use or require
setup.py
,setup.cfg
, orMANIFEST.in
. Those are for setuptools. Unless you are using setuptools, of course, which still usesMANIFEST.in
. You can convert the old files usingpipx run hatch new --init
or with ini2toml.
Selecting a backend
Backends handle metadata the same way, so the choice comes down to how you specify what files go into an SDist and extra features, like getting a version from VCS. If you don’t have an existing preference, hatchling is an excellent choice, balancing speed, configurability, and extendability.
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 = ["flit_core>=3.3"]
build-backend = "flit_core.buildapi"
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
pyproject.toml: project table
The metadata is specified in a standards-based format:
[project]
name = "package"
description = "A great package."
readme = "README.md"
license = "BSD-3-Clause"
license-files = ["LICENSE"]
authors = [
{ name = "My Name", email = "me@email.com" },
]
maintainers = [
{ name = "My Organization", email = "myemail@email.com" },
]
requires-python = ">=3.9"
dependencies = [
"typing_extensions",
]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Scientific/Engineering :: Physics",
]
[project.urls]
Homepage = "https://github.com/organization/package"
Documentation = "https://package.readthedocs.io/"
"Bug Tracker" = "https://github.com/organization/package/issues"
Discussions = "https://github.com/organization/package/discussions"
Changelog = "https://package.readthedocs.io/en/latest/changelog.html"
You can read more about each field, and all allowed fields, in packaging.python.org, Flit or Whey. Note that “Homepage” is special, and replaces the old url setting.
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
. Currently, hatchling>=1.26
, flit-core>=1.11
, pdm-backend>=2.4
, setuptools>=77
, and scikit-build-core>=0.12
support this. Only maturin
, meson-python
, and flit-core
do not support this yet.
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.
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
It is recommended to use dependency-groups instead of making requirement files. This allows you to specify dependencies that are only needed for development; unlike extras, they are not available when installing via PyPI, but they are available for local installation, and the dev
group is even installed by default when using uv
.
Here is an example:
[dependency-groups]
test = [
"pytest >=6.0",
]
dev = [
{ include-group = "test" },
]
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. You do not need to install the package, though usually you do (the high level uv
interface does). Nox, Tox, and cibuildwheel all support groups too. The dependency-groups
package provides tools to get the dependencies, too.
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:
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: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>F</mi><mi>o</mi><mi>r</mi><mi>m</mi><mi>a</mi><mi>t</mi><mo>:</mo></mrow><annotation encoding="application/x-tex">Format:%H</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.13889em;">F</span><span class="mord mathnormal" style="margin-right:0.02778em;">or</span><span class="mord mathnormal">ma</span><span class="mord mathnormal">t</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span></span></span></span>
node-date: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>F</mi><mi>o</mi><mi>r</mi><mi>m</mi><mi>a</mi><mi>t</mi><mo>:</mo></mrow><annotation encoding="application/x-tex">Format:%cI</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.13889em;">F</span><span class="mord mathnormal" style="margin-right:0.02778em;">or</span><span class="mord mathnormal">ma</span><span class="mord mathnormal">t</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span></span></span></span>
describe-name: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>F</mi><mi>o</mi><mi>r</mi><mi>m</mi><mi>a</mi><mi>t</mi><mo>:</mo></mrow><annotation encoding="application/x-tex">Format:%(describe:tags=true,match=*[0-9]*)</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.13889em;">F</span><span class="mord mathnormal" style="margin-right:0.02778em;">or</span><span class="mord mathnormal">ma</span><span class="mord mathnormal">t</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span></span></span></span>
And .gitattributes
(or add this line if you are already using this file):
.git_archival.txt export-subst
This 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.
- PDM info here.
- Setuptools still uses
MANIFEST.in
.
Flit will not use VCS (like git) to populate the SDist if you use standard tooling, even if it can do that using its own tooling. So make sure you list explicit include/exclude rules, and test the contents:
# Show SDist contents tar -tvf dist/*.tar.gz # Show wheel contents unzip -l dist/*.whl
Flit requires
license.file
to be set in your[project]
section to ensure it finds the license file.