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, or MANIFEST.in. Those are for setuptools. Unless you are using setuptools, of course, which still uses MANIFEST.in. You can convert the old files using pipx 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.

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.