pyproject.toml Release Gate (PEP 639)

Catches pyproject.toml metadata that breaks your next release: end-of-life requires-python, PEP 639 licence drift, classifier conflicts, setuptools too old for SPDX. Date-aware, offline. Runs entirely in your browser — nothing is uploaded.

Same engine as the VS Code extension, byte for byte.

Get the complete version $29

This page is the working piece. The full pack has everything below.

19 date-aware checks on the [project] metadata that decides whether your next release uploads

One hour of a senior Python developer's time costs more than the licence, in any market that publishes to PyPI — and without this the review has to happen by hand at every release.

Buy the full version — $29

· ReadyStack

Worked example

Real numbers from this tool, line by line.

pyproject.toml Release Gate (PEP 639)

Eight of the fourteen findings in one 26-line pyproject.toml are errors, and the maintainer who wrote it is a Python library author publishing wheels to PyPI from GitHub Actions. Nothing in it is a typo. Every line was correct when it was written.

Packaging metadata goes stale on a calendar, and neither your tests nor your build tools read a calendar.

The manifest, abbreviated:

[build-system] requires = ["setuptools>=61.0", "wheel"]

[project] version = "1.4.0" requires-python = ">=3.8,<4" license = "Apache 2.0" license-files = ["/usr/share/licenses/invoice-toolkit/LICENSE", "../COPYING"] dynamic = ["version"] classifiers = [ "Programming Language :: Python :: 3.7", "License :: OSI Approved :: Apache Software License", ]

Run it through the 19 checks in pyproject.toml Release Gate with today set to 2026-09-11 and you get fourteen findings: eight errors, five warnings, one note.

The licence block accounts for five. PEP 639 replaced the old license = { text = "MIT" } table and the License :: OSI Approved :: ... classifiers with one SPDX expression plus a license-files list, and setuptools implements that from 77.0.0. So license = "Apache 2.0" is not an SPDX identifier — the identifier is Apache-2.0. The License :: classifier sitting next to an SPDX expression is not a warning; that combination is refused and the build stops. Both license-files entries are rejected too, because PEP 639 accepts only relative globs inside the project, so an absolute path and a ../ escape are out. And setuptools>=61.0 is loose enough that a fresh build box can resolve a setuptools that does not know the license field exists — which is how one repo builds correctly on a laptop and wrong in CI.

Then the interpreter. requires-python = ">=3.8,<4" promises Python 3.8, whose upstream security support ended 2024-10-07 — 704 days before the date above. The Python :: 3.7 classifier advertises a version the same file's requires-python refuses, so pip and your project page disagree in public. The <4 cap is its own failure: on the next Python release, resolvers skip your package.

The last error is structural. version = "1.4.0" is static while version is also listed in dynamic. PEP 621 makes that a hard build error, not a preference.

The surprise: python -m build and twine check pass a manifest whose interpreter floor died two years ago, because staleness is not a syntax error. Asking an assistant is worse, because it answers from pre-PEP-639 documentation and has no reliable idea what today's date is — it hands back the same licence table and the same >=3.8 floor that caused the problem.

The date matters more than it looks. Take an otherwise clean manifest floored at requires-python = ">=3.10". On 2026-09-11 that is one warning: Python 3.10 reaches end of life 2026-10-31, fifty days out, so you bump the floor on your own schedule. On 2026-11-15 the same unchanged file is one error. The file did not move. The calendar did.

Finding out late has a price: a version number on PyPI cannot be re-used, so a bad release costs you a version, a changelog entry and an explanation.

The free tier finishes one job completely: all 19 checks on the file open in your editor, with the line, the spec that moved and the replacement text, plus the same engine as a browser page that uploads nothing. The licence changes scope and ownership rather than depth: every pyproject.toml in a monorepo in one pass, written to a dated report file that stays in the repo and goes into the pull request.

Questions people ask

What does pyproject.toml Release Gate actually do?

It reads the [project] and [build-system] tables of a pyproject.toml and reports 19 kinds of metadata that break a release: an interpreter floor past end of life, PEP 639 licence drift, classifiers that contradict requires-python, a setuptools pin too old to read the SPDX license field, and fields that are both static and dynamic. Each finding carries the line, the spec and the replacement text.

Who is this for?

Python library maintainers who publish wheels to PyPI from GitHub Actions, and platform engineers who own a monorepo with dozens of pyproject.toml files written by different people in different years. If you tag releases and a failed upload costs you a version number, the checks apply to you.

Why is a free tool or a chatbot not enough?

twine check, build and pip all accept a manifest whose interpreter floor died two years ago, because staleness is a calendar fact rather than a syntax error. A chatbot answers from pre-PEP-639 training data and has no reliable sense of today's date, so it returns the same license table and requires-python floor that caused the failure.

What is free and what needs a licence?

Free covers one file completely: all 19 checks on the pyproject.toml open in your editor, with line numbers and replacement text, plus the same engine as a browser page. The licence changes scope and ownership, not depth — every manifest in the workspace in one pass, written to a dated report file you keep in the repo.

What would this cost without the extension?

You would read PEP 621, PEP 639 and the CPython release cycle yourself, once per release, for every manifest. One hour of a senior Python developer's time costs more than the licence in any market that publishes to PyPI, and a re-used version number cannot be re-uploaded once a bad wheel lands.

Ask about this tool

One question, answered by the person who built it. Your email only if you want the answer sent.

Want the full version?
Enter your email and we send the download link.
ENDEJAESPT

Find a tool