Skip to content

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.

Python project: a pyproject.toml (preferred), setup.py, or setup.cfg at the repo root is required.

  • 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).

pyproject.toml has no project.name (PEP 621). Declare the distribution name so pip install . / uv build work.

pyproject.toml has no project.requires-python; declare a floor (e.g. >=3.10) so installs fail fast on unsupported interpreters.

Python module filenames should be snake_case (PEP 8).

Trojan Source (CVE-2021-42574): bidi override chars in Python sources are rejected.

The full ruleset definition is committed at crates/alint-dsl/rulesets/v1/python.yml in the alint repo (the snapshot below is generated verbatim from that file).

# alint://bundled/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.
version: 1
facts:
- id: has_python
any_file_exists:
- pyproject.toml
- "**/pyproject.toml"
- setup.py
- "**/setup.py"
- setup.cfg
- "**/setup.cfg"
- requirements.txt
- "**/requirements.txt"
rules:
# --- Manifest + lockfile ------------------------------------------
- id: python-manifest-exists
when: facts.has_python
# 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
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/"
- id: python-has-lockfile
when: facts.has_python
# 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
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).
# --- pyproject.toml shape -----------------------------------------
# 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
# (zero-match is a violation, which is what we want here).
- 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/"
- id: python-pyproject-declares-requires-python
when: facts.has_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
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.
# --- Source-file conventions --------------------------------------
- id: python-module-snake-case
when: facts.has_python
# 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
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"
# 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.
- 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: {}
- 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: {}
# --- Trojan Source defense on Python sources ----------------------
- 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/"