A dedicated linter and auto-fixer for pyproject.toml files.
Validates your pyproject.toml against PEP 517, 518, 621, and 735 standards, checks backend-specific schemas (setuptools, Poetry, Hatch, uv), audits dependencies, and validates classifiers — all in under 100ms.
pyproject.toml is now the universal Python project config, yet no dedicated tool validates it against PEP standards. Developers discover misconfigurations only at build time or when switching between backends. Common mistakes include deprecated keys, missing required fields, invalid classifiers, and backend-specific schema violations.
| Approach | Catches PEP violations | Backend schemas | Auto-fix | Classifier validation |
|---|---|---|---|---|
| Manual review | Sometimes | No | No | No |
| Build-time errors | Some | Partial | No | No |
| pyproject-lint | Yes | Yes | Yes | Yes |
pip install pyproject-lintOr with uv:
uv add --dev pyproject-lint# Validate your pyproject.toml
pyproject-lint check
# Auto-fix common issues
pyproject-lint fix
# Preview fixes without writing
pyproject-lint fix --dry-run
# Machine-readable output
pyproject-lint check --format jsonValidate a pyproject.toml file.
pyproject-lint check [PATH] [OPTIONS]
Arguments:
PATH Path to pyproject.toml (default: pyproject.toml)
Options:
--backend [setuptools|poetry|hatch|uv|auto]
Force a specific backend for validation (default: auto)
--format [text|json]
Output format (default: text)
Exit code 1 if errors are found, 0 otherwise.
Auto-fix common issues in a pyproject.toml file.
pyproject-lint fix [PATH] [OPTIONS]
Arguments:
PATH Path to pyproject.toml (default: pyproject.toml)
Options:
--dry-run Show what would change without writing
--backend [setuptools|poetry|hatch|uv|auto]
Force a specific backend for validation (default: auto)
- PEP 517/518:
[build-system]table — requires, build-backend - PEP 621:
[project]metadata — name, version, authors, dependencies, dynamic fields - PEP 735:
[dependency-groups]— structure, include-group references
- setuptools: Valid
[tool.setuptools]keys, deprecatednamespace-packages - Poetry: Valid
[tool.poetry]keys, missing section detection - Hatch: Valid
[tool.hatch]keys - uv: Valid
[tool.uv]keys, deprecateddev-dependencies
- Validates against the PyPI trove classifier database
- Detects typos and suggests corrections
- Flags duplicate classifiers
- Flags exact-pinned versions (
==) without flexibility - Detects duplicate dependencies
- Reports unsorted dependency lists (with auto-fix)
- Checks optional-dependencies for duplicates
Add to your .pre-commit-config.yaml:
repos:
- repo: https://github.com/your-org/pyproject-lint
rev: v0.1.0
hooks:
- id: pyproject-lintpyproject-lint check --format json{
"diagnostics": [
{
"rule_id": "pep621-metadata",
"severity": "error",
"message": "[project] must have a 'name' field (PEP 621).",
"key_path": "project.name"
}
],
"summary": {
"errors": 1,
"warnings": 0,
"fixable": 0
}
}- Clone the repository
- Install dependencies:
uv sync - Run tests:
uv run pytest - Run linter:
uv run ruff check .
src/pyproject_lint/
cli.py # Click CLI entry point
models.py # Diagnostic models (Severity, Diagnostic, LintResult)
parser.py # TOML parsing (tomli/tomllib)
registry.py # Rule registry and runner
fixer.py # Auto-fix engine
serializer.py # TOML serializer for writing fixes
rules/
pep.py # PEP 517/518/621/735 rules
backend.py # Backend-specific rules
classifiers.py # Classifier validation
dependencies.py # Dependency audit
Rules are functions decorated with @register_rule:
from pyproject_lint.models import Diagnostic, Severity
from pyproject_lint.registry import register_rule
@register_rule("my-custom-rule")
def check_something(data: dict) -> list[Diagnostic]:
diagnostics = []
if "project" in data and "name" not in data["project"]:
diagnostics.append(
Diagnostic(
rule_id="my-custom-rule",
severity=Severity.ERROR,
message="Missing project name.",
key_path="project.name",
)
)
return diagnosticsMIT