T tomlkit·org
Inspect Formatter Validator Lint Stats Keys Query Set value pyproject Convert TOMLJSON JSONTOML TOMLYAML YAMLTOML INITOML TOMLINI .envTOML TOML.env TOMLTS TOMLCSV CSVTOML TOMLXML TOML.properties .propertiesTOML Transform Sort keys Flatten Unflatten Redact Minify Generate Go struct Rust struct Python types JSON Schema Compare Diff Merge

pyproject.toml Validator

updated 26 August 2026

A pyproject.toml can be flawless TOML and still be rejected by every build backend there is, because [project] is not just a table — it is a closed schema with PEP 440 versions, PEP 508 dependency strings and a rule that a field is static or dynamic but never both. This checks the file against those rules and tells you the line number, not just that something is wrong somewhere.

PEP 517 / 518 / 621 / 440 / 508 / 639

The errors that cost the most time

Build failures from pyproject.toml are unusually expensive because the message you get rarely names the field. A backend reads the table, finds something it cannot use, and raises from three layers down. These are the ones worth recognising on sight.

Static and dynamic at once. PEP 621 says a field is written in the file or listed in dynamic so the backend supplies it. Both is an error, and it is easy to arrive at: you add dynamic = ["version"] for setuptools-scm and forget to delete the version line. setuptools' complaint does not say "delete one of these".

A version that is not PEP 440. 2026-08-26 is the common one — a date-based version has to be written with dots, 2026.8.26, because a dash means something else entirely. Worth separating from a version that is legal but is not the normalised spelling: 1.0-alpha1 and v1.0.0.rc1 are both accepted by PEP 440 and both normalise to something shorter (1.0a1, 1.0.0rc1). That normalised form is what PyPI shows and what the wheel filename contains, so the validator reports it as a note rather than an error — but if a colleague pins the spelling you typed, it will not be the string the index has.

A version written bare. version = 1.0 is a TOML float, and version = 1.0.0 is not valid TOML at all. It has to be a quoted string. This is the one case where the TOML parser and the schema disagree about which is the confusing error.

A requirement missing its operator. "requests 2.31" parses as a package name followed by nothing it recognises. "requests>=2.31" is what you meant. Likewise a -r requirements.txt line pasted in from pip: that is a pip flag, not a requirement, and it belongs in a requirements file.

Coming from Poetry

Poetry predates PEP 621, so files written for it put everything under [tool.poetry] in shapes that no longer match. Poetry 2 reads [project] when it is present and ignores its own table, which means a half-migrated file has two copies of the metadata and only one of them is used — usually not the one you edited.

The three shape changes: dependencies becomes an array of strings rather than a table of constraints; authors becomes an array of { name, email } tables rather than "Name <mail>" strings; and Poetry's caret and tilde constraints (^1.2) are not PEP 508 — the nearest equivalents are >=1.2,<2.0 and ~=1.2.

What it deliberately does not do

It does not resolve anything. No request goes anywhere, so it cannot tell you a package name is misspelled, a version does not exist, or two constraints cannot both be satisfied. What it can tell you is that a string is not a requirement, a classifier is not a trove classifier, and a field is not one PEP 621 defines — which is the whole class of failures you can fix without a network.