pyproject-doctor
Offline deep validator for pyproject.toml that catches the semantic mistakes other tools miss.
Most validators only check TOML syntax. pyproject-doctor goes further: it validates that your versions are PEP 440 compliant, your requires-python is a valid specifier set, your dependencies are PEP 508 compliant, your version constraints are actually satisfiable, your referenced files exist, your URLs are real URLs, your email addresses look right, and your entry-point references are correctly formatted.
What it checks
| Code | Description |
|---|---|
version-invalid |
project.version is not a valid PEP 440 version |
requires-python-invalid |
project.requires-python is empty, malformed, not a valid PEP 440 specifier set, or unsatisfiable (e.g. >=3.12,<3.8) |
dep-invalid |
A dependency in project.dependencies, project.optional-dependencies, or build-system.requires is not a valid PEP 508 requirement |
constraint-unsatisfiable |
A dependency's version specifiers form an impossible range (e.g. >=2.0,<1.0) |
file-missing |
A file referenced by project.readme, project.license, or an entry-point module does not exist |
url-invalid |
A value in project.urls is not a valid absolute URL |
email-invalid |
An author or maintainer email address is malformed |
entry-point-invalid |
A script or entry-point value is not in valid module:attr format |
classifier-unknown |
A classifier in project.classifiers is not a known trove classifier (requires pip install 'pyproject-doctor[classifiers]') |
dynamic-malformed |
project.dynamic is not a list of strings |
dynamic-name-forbidden |
name appears in project.dynamic (PEP 621 requires it to always be static) |
dynamic-field-unknown |
An entry in project.dynamic is not a recognized [project] field name |
dynamic-static-conflict |
A field listed in project.dynamic is also set statically in [project] |
license-expression-invalid |
project.license in its modern PEP 639 string form is not a valid SPDX license expression: empty, an unknown license or exception identifier, a deprecated identifier, the disallowed + suffix (use the -or-later form), a misplaced operator, or unbalanced parentheses. The legacy table form ({ file = ... } / { text = ... }) is not treated as an expression |
build-system-requires-invalid |
build-system.requires is missing, not a list, or an empty list, when [build-system] is present |
build-backend-package-missing |
A recognized build-system.build-backend (setuptools, hatchling, poetry-core, flit-core, pdm-backend, maturin, scikit-build-core, or meson-python) is declared, but its own package is not listed in build-system.requires. Skipped for unrecognized backends and for in-tree backends (backend-path set) |
build-backend-tool-mismatch |
[tool.poetry] declares project metadata (name, version, or dependencies) while build-system.build-backend is not a poetry-core backend, or [tool.hatch] has configuration while the backend is not hatchling.build |
poetry-pep621-conflict |
version or dependencies is set statically in both [project] and [tool.poetry], where the two tables use incompatible syntaxes and can silently diverge |
Install
pip install pyproject-doctor
Or with classifier validation:
pip install "pyproject-doctor[classifiers]"
Usage
# Validate pyproject.toml in the current directory
pyproject-doctor
# Validate a specific file
pyproject-doctor /path/to/pyproject.toml
# JSON output
pyproject-doctor --format json
# SARIF 2.1.0 output (for GitHub code scanning and other SARIF consumers)
pyproject-doctor --format sarif
Exit code is 1 if any error-level diagnostic is found, 0 otherwise.
The sarif format emits a SARIF 2.1.0 log: each diagnostic becomes one result, the
diagnostic code is the ruleId, every result points at the analyzed pyproject.toml,
and the SARIF level (error, warning, note) is derived from the diagnostic's level.
Pre-commit
Add to .pre-commit-config.yaml:
repos:
- repo: https://github.com/amaar-mc/pyproject-doctor
rev: v0.5.0
hooks:
- id: pyproject-doctor
Example output
error project.version: version-invalid: 'not.a.version' is not a valid PEP 440 version
error project.dependencies[0]: constraint-unsatisfiable: Dependency 'requests': constraint '>=3.0,<2.0' is unsatisfiable (lower bound 3.0 exceeds upper bound 2.0)
error project.urls.Homepage: url-invalid: URL 'not-a-url' is not a valid absolute URL (must have scheme and host)
License
MIT. Copyright (c) 2026 Amaar Chughtai.
Metadata
Release files for pyproject-doctor 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyproject_doctor-0.5.0.tar.gz | 962.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyproject_doctor-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 987.9 kB
Release files / pyproject_doctor-0.5.0.tar.gz
| Download URL | pyproject_doctor-0.5.0.tar.gz |
|---|---|
| Size | 962.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e2f16fb7f51fae6dcfa087f1ea1adb4a13d50a6785229f641cbd7ad600194e36
|
|
BLAKE2b-256 checksum How to use checksums |
9105d932ae7254c6067c5a4bf2ecb9d47e1e84f6aff28951b7162ec8268fd011
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Release files / pyproject_doctor-0.5.0-py3-none-any.whl
| Download URL | pyproject_doctor-0.5.0-py3-none-any.whl |
|---|---|
| Size | 25.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d73e322ac5768b1abb4c8ff6a41c2453e0059e44be78f1d9ce214b97e923f179
|
|
BLAKE2b-256 checksum How to use checksums |
774520617ca53b949a2dd2f7a572e9364c7e8148f1297fd9130ccdcac34f20e9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|