I suspect they haven’t needed that because of the implicit default.
Setuptools has two build backends:
- The “normal” one that you have to use explicitly.
- The "“legacy” one that is used implicitly by default.
At the implementation level, the legacy one wraps the normal one, with the only behavior difference being to modify sys.path prior to the ‘normal” one being used. So anything the “normal” one supports, the “legacy” one also supports.
You get… “something” if you create an empty directory with nothing but an empty pyproject.toml file:
~/projects
❯ mkdir t
~/projects
❯ cd t
~/projects/t
❯ touch pyproject.toml
~/projects/t via 🐍 v3.13.3
❯ pyproject-build .
...
Successfully built unknown-0.0.0.tar.gz and unknown-0.0.0-py3-none-any.whl
A less silly example is filling out some basic information in the pyproject.toml file
~/projects/t via 🐍 v3.13.3
❯ echo '[project]\nname="foo"\nversion="1.0"\n' > pyproject.toml
~/projects/t is 📦 v1.0 via 🐍 v3.13.3
❯ mkdir foo
~/projects/t is 📦 v1.0 via 🐍 v3.13.3
❯ touch foo/__init__.py
~/projects/t is 📦 v1.0 via 🐍 v3.13.3
❯ pyproject-build .
...
Successfully built foo-1.0.tar.gz and foo-1.0-py3-none-any.whl
~/projects/t is 📦 v1.0 via 🐍 v3.13.3 took 8s
❯ unzip -l dist/foo-1.0-py3-none-any.whl
Archive: dist/foo-1.0-py3-none-any.whl
Length Date Time Name
--------- ---------- ----- ----
0 03-14-2026 16:09 foo/__init__.py
45 03-14-2026 16:10 foo-1.0.dist-info/METADATA
91 03-14-2026 16:10 foo-1.0.dist-info/WHEEL
4 03-14-2026 16:10 foo-1.0.dist-info/top_level.txt
340 03-14-2026 16:10 foo-1.0.dist-info/RECORD
--------- -------
480 5 files
It seems likely to me that there are a non zero number of projects out there implicitly relying on this behavior. Maybe they shouldn’t be, but I am sure that they are. If we want to potentially break those, then someone should probably at least spend some time looking at tarballs on PyPI to figure out what the level of impact is likely to be.
Personally, I think there’s probably a few options to move this forward if someone wants to:
- Focus on improving the
pip install . case specifically, that’s a subset of the general case (and thus is bound to be smaller), but pip would be within their rights to say they don’t want to diverge pip install . and pip install some-sdist.
- Focus on improving the error message in setuptools, but they’d be within their rights to say that they don’t want to do that (and tbh, it’s unclear to me how they’d differentiate from my foo case and Brett’s case to present a better error message here).
- Gather information to prove that the impact of removing the fallback (either with or without conditions) is small and unlikely to cause much disruption and write a PEP justifying that.
- Figure out how to get the ecosystem to transition to new packages being required to declare their build backend, so that hopefully, at some point, the packages that don’t declare their build backend are all old, legacy packages and hopefully are no longer important enough to need to keep working by default.
The first one of those, I think it’s not hard for pip to differentiate internally between pip install . and pip install some-sdist (in a conceptual sense, since they have the information to do that… but it may require plumbing that information through and that may be hard). That does kind of go against what PEP 517 requires though, and given it’s a breaking change, I suspect that pip wouldn’t want to do that “on it’s own”.
I suspect the 2nd one of those is a non-starter because I’m not sure how you’d tell the difference between “I’ve accidentally invoked setuptools” and “I’m relying on the implicit default of setuptools”… except maybe it could fail instead of use an unknown name? But even that feels like it’s kinda fixing one tiny corner of the “problem” and not really fixing the problem at all.
The third one of those I think would be good to do regardless, but I suspect the answer is going to be that more projects than we expect are relying on that implicit default (but I could be wrong! that’s why it’s good to collect data).
I suspect that the fourth option is the actual path forward (but again, that’s just my personal opinion!).
Some ideas for how to do that:
- Have PyPI enforce that a build-system must be declared for new sdists (probably warning at first, then transition to enforcing).
- Have build backends rewrite the
pyproject.toml file when they produce a sdist to ensure that the [build-system] key is correctly configured.
- This might only actually affect
setuptools given it’s default nature, but it might also effect other tools if they’re primarily being used directly rather than through a generic build tool like python -m build.
- This concept might be generally useful for other things, for instance cargo rewrites
Cargo.toml files to ensure that published artifacts only use TOML 1.0 semantics while developers can use TOML 1.1 (or later hypothetically) semantics.
That doesn’t actually solve the problem Brett had, but it stops the bleeding so maybe some day we could?
Although to be honest, I’m not sure that having a default is a bad thing either. setuptools is a perfectly fine build backend for simple projects. If you’re doing something complex there may be build backends that are better suited, but for a lot of things it’s perfectly fine.
I know I’ve personally omitted the [build-system] table from a package because it was a pure Python thing, so basically every build backend will work fine, and it’s simpler to not fill it out and let the defaults take over than to go look up what the right magic string is to chuck into that field.
Of course that wouldn’t be affected by setuptools rewriting the pyproject.toml to be explicit
and if it did that, then it also wouldn’t be affected by PyPI requiring that key to be filled out… but neither of those actually solve Brett’s problem either, they just make it more likely we can at some point.