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.

8 rules. Each links to its section below, which explains the check and shows its definition.

RuleReports
python-manifest-exists
error
Python project: a pyproject.toml (preferred), setup.py, or setup.cfg at the repo root is required.
python-has-lockfile
warning
A lockfile should be committed for reproducible installs (uv.lock / poetry.lock / Pipfile.lock / pdm.lock).
python-pyproject-declares-name
warning
pyproject.toml has no project.name (PEP 621).
python-pyproject-declares-requires-python
info
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
info
Python module filenames should be snake_case (PEP 8).
python-sources-final-newline
info
File must end with a single \n.
python-sources-no-trailing-whitespace
info
No line may end with space or tab.
python-sources-no-bidi
error
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.

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.

Python project: a pyproject.toml (preferred), setup.py, or setup.cfg at 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/"

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

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

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

- 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_]*.

pyproject.toml has no project.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.

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.

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"

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 by alint 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 by alint 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: {}

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"

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: error

To 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.

The full ruleset definition, comments included, is committed at crates/alint-dsl/rulesets/v1/python.yml in the alint repo.