[Docs] Need migration guide for license expression (PEP 639) about TOML-table-based `project.license` deprecation if v77 unavailable
Summary
TL;DR: TOML-table-based project.license is deprecated in the newest setuptools. But the highest supported setuptools on Python 3.8 only supports TOML-table-based project.license.
setuptools v77.0.0 adds license expression (PEP 639) support in:
- #4829
- #4706
cc @abravalheri @cdce8p
For pyproject.toml with license = { text = "expression" }:
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "xxx"
description = "XXX"
readme = "README.md"
requires-python = ">= 3.8"
license = { text = "Apache-2.0" }
It will emit a warning during installation:
/.../site-packages/setuptools/config/_apply_pyprojecttoml.py:82: SetuptoolsDeprecationWarning: `project.license` as a TOML table is deprecated
!!
********************************************************************************
Please use a simple string containing a SPDX expression for `project.license`. You can also use `project.license-files`.
By 2026-Feb-18, you need to update your project and remove deprecated calls
or your builds will no longer be supported.
See https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license for details.
********************************************************************************
!!
However, setuptools v77.0.0 does not support Python 3.8. The maximum supported version of Python 3.8 is v75.3.2 (v75.4.0 removed Python 3.8 support).
If developers change project.license = "Apache-2.0" as the warning suggested (emitted on a Python >=3.9 environment).
[project]
requires-python = ">= 3.8"
- license = { text = "Apache-2.0" }
+ license = "Apache-2.0"
The build will break on Python 3.8 due to the lack of PEP-639-supported setuptools (maximum is v75.3.2).
Processing /.../xxx
Installing build dependencies ... done
Getting requirements to build wheel ... error
error: subprocess-exited-with-error
× Getting requirements to build wheel did not run successfully.
│ exit code: 1
╰─> [87 lines of output]
configuration error: `project.license` must be valid exactly by one definition (2 matches found):
- keys:
'file': {type: string}
required: ['file']
- keys:
'text': {type: string}
required: ['text']
DESCRIPTION:
`Project license <https://peps.python.org/pep-0621/#license>`_.
GIVEN VALUE:
"Apache-2.0"
OFFENDING RULE: 'oneOf'
DEFINITION:
{
"oneOf": [
{
"properties": {
"file": {
"type": "string",
"$$description": [
"Relative path to the file (UTF-8) which contains the license for the",
"project."
]
}
},
"required": [
"file"
]
},
{
"properties": {
"text": {
"type": "string",
"$$description": [
"The license of the project whose meaning is that of the",
"`License field from the core metadata",
"<https://packaging.python.org/specifications/core-metadata/#license>`_."
]
}
},
"required": [
"text"
]
}
]
}
OS / Environment
Not related.
Additional Information
pyproject.toml is a static configuration file. It is impossible for packages that can support multiple Python versions (including EOLs) and also do not emit warnings during building. The warning can suggest developers use setup.py but I don't think it is encouraged since they are already using pyproject.toml.
I'd suggest to do not deprecate TOML-table-based project.license with only a text key. Make the following equivalent:
[project]
license = { text = "SPDX expression" }
[project]
license = "SPDX expression"
Code of Conduct
- [x] I agree to follow the PSF Code of Conduct
Please note that it is not in the scope of this project to support use cases involving discontinued Python versions.
Python 3.8 reached EoL in 2024, so use cases involving 3.8, are no longer supported by setuptools dev team.
If you wish to use the same codebase to support 3.8 and the following Python versions, I suppose you have a couple of options:
- Use the same (old) version of setuptools across the board and only do the changes when you can stop publishing your package for 3.8
- Use
dynamicand solve the license specification insetup.py - Live with the deprecated warning and plan discontinuing your package for 3.8 before the deprecation due date.
- Remove license expressions from the configuration files and instead rely on the automatic license file inclusion for the well-known license file paths (e.g.,
LICENSEat the root of the project). You would still be distributing license info in the form of license files (which at the end of the day is what counts... SPDX indexes are at the best an approximate proxy for the license file, re-distributors would still have to read the file themselves).
These suggestions are untested. Please make sure to verify and test before adopting.
Note this issue does not only exist for Python 3.8 but also all existing Python packages using setuptools. The string-based project.license is not backward-compatible (see the error in https://github.com/pypa/setuptools/issues/4903#issue-2936461540). Developers need to pin build-system.requires if they want to migrate to PEP 639:
[build-system]
requires = ["setuptools >= 77"]
build-backend = "setuptools.build_meta"
[project]
license = "SPDX expression"
However, this ideal setting will not work in the real world. For example, there are many use cases to use pip install --no-build-isolation due to limited internet connection (on a server) or build-system.requires contains big dependency (e.g., torch). The installer will not upgrade setuptools to the PEP 639 supported version (>= 77). A PEP-639-migrated package will break on the user side. Developers have to revert the migration.
Due to this limitation, downstream developers will postpone the PEP 639 migration. The old code will exist until the day the compact layer for table-based project.license is removed and breaks developers' CI.
If users want to make sure a version of setuptools that supports a certain feature is being picked up by the build frontend, it does make sense to use a >= for the version specification.
If the environment cannot download the version of the build requirements on the spot, then it needs to be pre-prepared beforehand.
These are normal activities when working on builds, not specific to this release. So in a sense, there is no need for a special guide for v77. The same arguments can be used for the whole parsing of pyproject.toml or setup.cfg before that. People usually omit the >= part because very often the build frontend will download the latest version of setuptools. If they need builds with special requirements (like lack of internet or opt-out of build isolation), then they need to be more careful and ensure the environment is properly set.
If you would to submit a docs PR with improvements that is very welcome, but other than, the release in question is not different than other features previously released.
https://github.com/pypa/setuptools/pull/4904 documents the version introducing PEP 639, so the information is available in the docs and warnings.
Further PRs for documentation improvements and suggestions are welcome.
#4904 documents the version introducing PEP 639, so the information is available in the docs and warnings.
Thanks! A user-friendly warning can prompt the developers to add a command to upgrade setuptools in their installation guide:
python3 -m pip install --upgrade pip setuptools
python3 -m pip git+https://github.com/owner/some-package
python3 -m pip another-package
Further PRs for documentation improvements and suggestions are welcome.
I wonder if it is suitable to automatically convert project.license = { text = "SPDX" } to project.license = "SPDX". I can submit a PR to implement this if accepted.
https://github.com/pypa/setuptools/blob/7c859e017368360ba66c8cc591279d8964c031bc/setuptools/config/_apply_pyprojecttoml.py#L201-L224
def _license(dist: Distribution, val: str | dict, root_dir: StrPath | None):
from setuptools.config import expand
if isinstance(val, dict) and set(val) == {'text'} and isinstance(val["text"], str):
# This is for backward compatibility with the old table-based `license` field.
# Convert the old table-based `license` to a string (PEP 639) if it only contains `text`
# and no `file` field.
# E.g. `license = {text = "MIT"}` to `license = "MIT"`
from ._validate_pyproject.formats import SPDX
if SPDX(val['text']):
val = val['text']
if isinstance(val, str):
...
else:
...
The deprecation warning will be still emitted on:
[project]
license = { text = "non SPDX expression" }
or
[project]
license = { file = "/path/to/file" }
A user-friendly warning can prompt the developers to add a command to upgrade setuptools in their installation guide:
Adding this instruction could be confusing for general users.
Most of the users don't need setuptools to be installed. And very often it is good that they don't (e.g. to avoid errors when the build isolation from frontend tools are leaky).
The default behaviour of build tools when the file pyproject.toml file exists is to download the build dependencies on the spot.
People that need to use --no-build-isolation (which is not the default) need to add this information to their own package README, because they require special circumstances to build.
I wonder if it is suitable to automatically convert project.license = { text = "SPDX" } to project.license = "SPDX".
The thing is that not all project.license.text are SPDX indices, and also some of them may coincidentally be parseable as an SPDX index but the developer may not want it to be interpreted as such. For example, a project may have project.license.text = "MIT", because the original code itself is MIT, but the project may contain bundled code under GPL: while "MIT" is parseable, the actual SPDX index would be "MIT AND GPL-3.0-or-later".
The reason why we have a warning is that we are prompting the user with a call to action, since it is not trivial and immediate to convert between one and the other. I think the best in the case is to refuse to guess, given that it is a form of legal information.
Please note that it is not in the scope of this project to support use cases involving discontinued Python versions.
Is it in the scope of the project to support use cases involving backwards compatibility across the project itself? Debian's Trixie comes with setuptools 75.8.0-1. This means that there is currently no documented way to package something for both modern Python versions and stable Debian in a uniform way AFAIU.
The present issue was opened on Mar 20; a few days later we had #4910 "breaking the internet". The present issue is about a different field, but the consequences would seem to be similar. Does this mean the present issue should also be seen in a different light?
Related:
- #4921
- #4919
For me, because of the issue here (impossibility to support old Python versions) as well as #4938, PEP639 is going to have the opposite effect than what it intended (improving metadata): I plan to just strip out all license metadata of my packages and just rely on the shipped LICENSE.txt as legal info.
Although not documented, the license_expression key can be used in setup.py and setup.cfg.
But can I use this?
Is there a reason the deprecation window needs to be so short? There are still many supported releases of Linux distros whose setuptools is too old to understand the new license format. In a few years, when these distro versions fall out of support, it'll be safe for setuptools to drop support for the old format.
Hi there. Has the problem been solved anywhere above? I'm trying to install a package on AIX and I ended up here due to the aforementioned error. I have setuptools 58.1.0. Thank you.
These are normal activities when working on builds, not specific to this release. So in a sense, there is no need for a special guide for v77. The same arguments can be used for the whole parsing of
pyproject.tomlorsetup.cfgbefore that. People usually omit the>=part because very often the build frontend will download the latest version of setuptools. If they need builds with special requirements (like lack of internet or opt-out of build isolation), then they need to be more careful and ensure the environment is properly set.
I correctly pin like that. The problem I have is that this change requires me to add some further constraint setuptools < <whatever version will be released on February 2026> for all my releases to still work in the future without me having to push a new one. With such backward-incompatible changes, basically, all releases on PyPI that specify a license with the old format and do not pin with either setuptools =~ <version> or setuptools < <version> will not be installable anymore without a new release by a maintainer, or am I missing something?
Which version should I roughly pin to? I don't want to use setuptools =~ <version> because old setuptools version may also not be released for newer Python versions anymore, understandably so. But, I think one of the major issues with setuptools is how fast-moving the major version releases are. 1 year ago setuptools 70 was released. The current release is 80. So roughly 10 major version releases per year, so I can roughly pin to setuptools < 86 to somewhat future-prove my releases?
The present issue was opened on Mar 20; a few days later we had #4910 "breaking the internet". The present issue is about a different field, but the consequences would seem to be similar. Does this mean the present issue should also be seen in a different light?
Personally, I would strongly agree.
@abravalheri @jaraco So as I understand extending the deprecation window for supporting a TOML "table", or extending support for any other mechanism that works on 3.8+, is not being considered? 👿
Edit: I noticed this request was also submitted by @befeleme as a new issue: #5081.