--- title: Python packaging with pyproject.toml and setuptools date: 2023-11-04 tags: [code, python] description: Python packaging has been in a bad state for ages. In this post I am going to explain how I do package management without loosing my mind. --- Python packaging has been in a bad state for ages. I recently read a [post by Gregory Szorc](https://gregoryszorc.com/blog/2023/10/30/my-user-experience-porting-off-setup.py/) that resonated with me a lot. Still, I do not personally have any issues in practice. So in this post I am going to explain how I do package management without loosing my mind. ## Be aware of the different kinds of tools in package management Package management in python is highly modular, and each part of the process can have multiple implementations. In this article I will use `setuptools` as a build backend (the part that actually builds the package), `build` as a build frontend (the part that creates a build environment) and `pip` to install packages. But I could also use `poetry` to cover all of those roles with a single tool. There are standards that allow all of these tools to work together. [PEPĀ 427](https://peps.python.org/pep-0427/) defines the "wheel" package format. [PEPĀ 517](https://peps.python.org/pep-0517/) defines the interface between build backends and frontends. Note that the separation is not always razor sharp. For example, a build frontend may also have to install packages into the build environment. And an installer might have to act as a build frontend if the package is not available as a wheel. For an excellent overview of the different tools and options for package management, see [this post by Anna-Lena Popkes](https://alpopkes.com/posts/python/packaging_tools/). ## Don't create a package if you want an environment Many modern package managers like npm, cargo, or Poetry automatically create lockfiles and recommend to commit them to version control. I really don't understand why they are doing this. A package is supposed to be installed along with other packages, so it needs to be compatible with as many versions as possible. I do understand that you sometimes want a reproducible environment. But those are two separate things. If you want to create a reproducible environment, you can use a simple [requirements.txt file](https://pip.pypa.io/en/stable/reference/requirements-file-format/) and install it with `python -m pip install -r requirements.txt`. The file could look like this: ``` # allow a range of versions foo >= 1.1, < 2.0 # select optional features bar[feature] # pin a specific version and specifiy a hash for supply chain integrity baz == 1.2.3 --hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d ``` There are some tools that can help you generate these files, e.g. `pip freeze` or [pip-tools](https://github.com/jazzband/pip-tools). Still, I find that you should not have too much automation in this area. The goal is that you have control over the environment, not the other way around. ## Use venv instead of virtualenv You usually want to install the requirements for each project into a separate environment. That approach was pioneered by the package [`virtualenv`](https://virtualenv.pypa.io/en/latest/). However, the functionality was so useful that it was integrated into the standard library in python 3.3 (2012). I still see references to `virtualenv` more than 10 years later, but you really don't need it. Just run `python -m venv` instead. ## Use pyproject.toml to specify package meta data I stuck with `setup.py` and `setup.cfg` pretty long. The most important reason was that editable installs were not supported when using pyproject.toml. Fortunately, this is not a problem because setuptools can still read meta data from those files. But the future is pyproject.toml and both setuptools (>= 64) and pip (>= 21.3) now support editable installs. Note that [Ubuntu 22.04 is still on setuptools 59](https://packages.ubuntu.com/jammy/python3-setuptools). I have started porting some projects to the new system. But I will probably wait with some more critical projects until the new features are widely available. The [setuptools documentation](https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html) on pyproject.toml is solid and porting an existing `setup.py` or `setup.cfg` to the new syntax should be simple enough. You can also use [`ini2toml`](https://github.com/abravalheri/ini2toml) to automatically do the conversion. Once you have created that file you can build your package either by using `python -m build` (which I see recommended in most places) or `python -m pip wheel .` (which doesn't require an additional tool). To upload your package to PyPI you can use [twine](https://twine.readthedocs.io/en/stable/). ## Include data files setuptools will automatically include python files in the package. If you need to include other files, e.g. templates or translations, you traditionally had to use a separate `MANIFEST.in` file. That still works, but it can also be included in pyproject.toml directly: ``` [tool.setuptools.package-data] mypackage = [ "**/*.html", "**/*.csv", ] ``` ## Use backend-specific configuration for more complex packages So far we discussed pure python packages. Packages that contain C code or similar are much more complicated for several reasons. First because they need an additional compile step, and second because we need to build different binary packages for different architectures. For setuptools you [still configure that in `setup.py`](https://setuptools.pypa.io/en/latest/userguide/ext_modules.html). (`setup.py` is not deprecated. It is just no longer necessary for simple packages.) However, there are other, more specialized build backends like [scikit-build-core](https://github.com/scikit-build/scikit-build-core) or [meson-python](https://meson-python.readthedocs.io/en/latest/tutorials/introduction.html). ## Configure other tools Most tools can be configured using pyproject.toml, e.g. [pytest](https://docs.pytest.org/en/7.1.x/reference/customize.html#pyproject-toml), [coverage](https://coverage.readthedocs.io/en/latest/config.html), or [isort](https://pycqa.github.io/isort/docs/configuration/config_files.html#pyprojecttoml-preferred-format). A prominent exception is [flake8](https://github.com/PyCQA/flake8/issues/234#issuecomment-812800722), but you can replace most of it by [ruff](https://docs.astral.sh/ruff/configuration/#using-pyprojecttoml). ## Conclusion Python's packaging infrastructure is certainly not it's best feature, but it is still usable. The transition to pyproject.toml took far too long and was far too messy, but I am confident that we will finally be done with it in just a few years. In this article I stuck with setuptools, because that is the build backend I know best. However, setuptools has accumulated a lot of legacy code over the years. [flit](https://flit.pypa.io/en/latest/rationale.html) is another backend that has "not being setuptools" as its main feature. I wouldn't say that python packaging is good now. But at least it has stabilized to a degree that I feel like we could actually, finally reap the benefits of blowing up everything.