python@v1
Hygiene checks for Python projects (pyproject / setuptools / Poetry / PDM / uv). Adopt it with:
extends: - alint://bundled/python@v1 Gated with when: facts.has_python (true if any standard Python
project marker exists anywhere in the tree) plus a per-rule
scope_filter: { has_ancestor: [pyproject.toml, setup.py, requirements.txt] } on per-file content rules so they only
apply to files inside a Python package — useful in polyglot
monorepos where Python packages sit alongside Rust / Node / Go
subdirectories. Override has_python with your own facts:
block if you need a different heuristic.
What it checks
Section titled "What it checks"8 rules. Each links to its section below, which explains the check and shows its definition.
| Rule | Reports |
|---|---|
python-manifest-existserror | Python project: a pyproject.toml (preferred), setup.py, or setup.cfg at the repo root is required. |
python-has-lockfilewarning | A lockfile should be committed for reproducible installs (uv.lock / poetry.lock / Pipfile.lock / pdm.lock). |
python-pyproject-declares-namewarning | pyproject.toml has no project.name (PEP 621). |
python-pyproject-declares-requires-pythoninfo | pyproject.toml has no project.requires-python; declare a floor (e.g. >=3.10) so installs fail fast on unsupported interpreters. |
python-module-snake-caseinfo | Python module filenames should be snake_case (PEP 8). |
python-sources-final-newlineinfo | File must end with a single \n. |
python-sources-no-trailing-whitespaceinfo | No line may end with space or tab. |
python-sources-no-bidierror | Trojan Source (CVE-2021-42574): bidi override chars in Python sources are rejected. |
All 8 rules run only when facts.has_python holds, so the ruleset stays quiet in repositories it doesn't apply to.
python-manifest-exists
Section titled "python-manifest-exists"Modern Python wants pyproject.toml (PEP 518 / 621), but setup.py + setup.cfg are still the reality in older code bases. Any one of the three satisfies the rule.
- kind:
file_exists - level:
error - when:
facts.has_python - policy: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/
Python project: a
pyproject.toml(preferred),setup.py, orsetup.cfgat the repo root is required.
- id: python-manifest-exists when: facts.has_python kind: file_exists paths: ["pyproject.toml", "setup.py", "setup.cfg"] root_only: true level: error message: >- Python project: a `pyproject.toml` (preferred), `setup.py`, or `setup.cfg` at the repo root is required. policy_url: "https://packaging.python.org/en/latest/guides/writing-pyproject-toml/" python-has-lockfile
Section titled "python-has-lockfile"Accept any of the common lockfile names from uv / Poetry / Pipenv / PDM. At least one should be committed for reproducible installs. Skip this rule (level: off) if your project genuinely pins via requirements.txt with hashes.
- kind:
file_exists - level:
warning - when:
facts.has_python
A lockfile should be committed for reproducible installs (uv.lock / poetry.lock / Pipfile.lock / pdm.lock).
- id: python-has-lockfile when: facts.has_python kind: file_exists paths: - uv.lock - poetry.lock - Pipfile.lock - pdm.lock root_only: true level: warning message: >- A lockfile should be committed for reproducible installs (uv.lock / poetry.lock / Pipfile.lock / pdm.lock). python-pyproject-declares-name
Section titled "python-pyproject-declares-name"Both rules are scoped to pyproject.toml — if a project still lives on setup.py / setup.cfg without a pyproject.toml at all, neither rule evaluates and the project passes. When pyproject.toml does exist but the enforced field is missing, the rule fires (no match is a violation, which is the point here).
- kind:
toml_path_matches - level:
warning - when:
facts.has_python - policy: https://peps.python.org/pep-0621/
pyproject.tomlhas noproject.name(PEP 621). Declare the distribution name sopip install ./uv buildwork.
- id: python-pyproject-declares-name when: facts.has_python kind: toml_path_matches paths: pyproject.toml path: "$.project.name" matches: '^[A-Za-z][A-Za-z0-9._-]*$' level: warning message: >- `pyproject.toml` has no `project.name` (PEP 621). Declare the distribution name so `pip install .` / `uv build` work. policy_url: "https://peps.python.org/pep-0621/" python-pyproject-declares-requires-python
Section titled "python-pyproject-declares-requires-python"Bracket notation is required on keys with dashes — JSONPath dot-notation segments are restricted to [A-Za-z_][A-Za-z0-9_]*.
- kind:
toml_path_matches - level:
info - when:
facts.has_python
pyproject.tomlhas noproject.requires-python; declare a floor (e.g.>=3.10) so installs fail fast on unsupported interpreters.
- id: python-pyproject-declares-requires-python when: facts.has_python kind: toml_path_matches paths: pyproject.toml path: "$.project['requires-python']" matches: '^.+$' level: info message: >- `pyproject.toml` has no `project.requires-python`; declare a floor (e.g. `>=3.10`) so installs fail fast on unsupported interpreters.python-module-snake-case
Section titled "python-module-snake-case"PEP 8: module filenames are lowercase with underscores. Scoped to src/** and the top level only — tests commonly use test_*.py (already snake) and conftest.py which are fine.
- kind:
filename_case - level:
info - when:
facts.has_python - policy: https://peps.python.org/pep-0008/#package-and-module-names
Python module filenames should be snake_case (PEP 8).
- id: python-module-snake-case when: facts.has_python kind: filename_case paths: ["*.py", "src/**/*.py" ] case: snake level: info message: "Python module filenames should be snake_case (PEP 8)." policy_url: "https://peps.python.org/pep-0008/#package-and-module-names" python-sources-final-newline
Section titled "python-sources-final-newline"Changed in v0.9.18: the cosmetic-formatting rules (final-newline + no-trailing-whitespace) default-exclude the canonical test-fixture trees. Linters and test-suite repos (cpython Lib/test/**, ruff crates/*/resources/test/fixtures/**, pytest-style tests/fixtures/**, Go-style **/testdata/**) carry deliberately-malformed Python files; the rules can't distinguish "real source" from "intentional bad input" so the exclude defaults narrow the scope. Users with an unconventional fixture layout can override via per-rule paths: in their config. The Trojan-Source check (python-sources-no-bidi) deliberately keeps the wider scope — bidi-control chars in fixtures are still a security concern.
- kind:
final_newline - level:
info - when:
facts.has_python - fix:
file_append_final_newline(applied byalint fix)
- id: python-sources-final-newline when: facts.has_python kind: final_newline paths: include: ["**/*.py" ] exclude: - "tests/fixtures/**" - "**/tests/fixtures/**" - "**/test/fixtures/**" - "**/testdata/**" - "Lib/test/**" - "crates/**/resources/**" scope_filter: has_ancestor: [pyproject.toml, setup.py, requirements.txt] level: info fix: file_append_final_newline: {}python-sources-no-trailing-whitespace
Section titled "python-sources-no-trailing-whitespace"- kind:
no_trailing_whitespace - level:
info - when:
facts.has_python - fix:
file_trim_trailing_whitespace(applied byalint fix)
- id: python-sources-no-trailing-whitespace when: facts.has_python kind: no_trailing_whitespace paths: include: ["**/*.py" ] exclude: - "tests/fixtures/**" - "**/tests/fixtures/**" - "**/test/fixtures/**" - "**/testdata/**" - "Lib/test/**" - "crates/**/resources/**" scope_filter: has_ancestor: [pyproject.toml, setup.py, requirements.txt] level: info fix: file_trim_trailing_whitespace: {}python-sources-no-bidi
Section titled "python-sources-no-bidi"- kind:
no_bidi_controls - level:
error - when:
facts.has_python - policy: https://trojansource.codes/
Trojan Source (CVE-2021-42574): bidi override chars in Python sources are rejected.
- id: python-sources-no-bidi when: facts.has_python kind: no_bidi_controls paths: "**/*.py" scope_filter: has_ancestor: [pyproject.toml, setup.py, requirements.txt] level: error message: "Trojan Source (CVE-2021-42574): bidi override chars in Python sources are rejected." policy_url: "https://trojansource.codes/" The when: clauses above read these facts. Each is resolved once per run; alint facts prints what they resolved to in your repository.
facts: - id: has_python any_file_exists: - pyproject.toml - "**/pyproject.toml" - setup.py - "**/setup.py" - setup.cfg - "**/setup.cfg" - requirements.txt - "**/requirements.txt" Customize
Section titled "Customize"Every rule here can be overridden by id from your own .alint.yml: change its level, or set level: off to drop it. An id that doesn't exist is an error at config load, so a typo can't silently pass.
extends: - alint://bundled/python@v1 rules: - id: python-manifest-exists level: off - id: python-has-lockfile level: errorTo take only part of the ruleset, filter it where you extend it with only: or except::
extends: - url: alint://bundled/python@v1 except: [python-manifest-exists]Bundled rulesets covers versioning and how rulesets combine.
Source
Section titled "Source"The full ruleset definition, comments included, is committed at crates/alint-dsl/rulesets/v1/python.yml in the alint repo.