Linting¶
Before writing any code, set up formatters and linters.
In general, add all project configuration to pyproject.toml
. Do not use setup.cfg
, setup.py
, .editorconfig
or tool-specific files like .coveragerc
or pytest.ini
.
Configuration¶
New projects should use the Ruff formatter and linter, with line lengths of 119 (the Django coding style until 4.0). A starting point, based on script.sh in standard-maintenance-scripts:
[tool.ruff]
line-length = 119
target-version = "py311"
[tool.ruff.lint]
select = ["ALL"]
ignore = [
"ANN", "C901", "COM812", "D203", "D212", "D415", "EM", "ISC001", "PERF203", "PLR091", "Q000",
"D1",
]
[tool.ruff.lint.flake8-builtins]
builtins-ignorelist = ["copyright"]
[tool.ruff.lint.flake8-unused-arguments]
ignore-variadic-names = true
[tool.ruff.lint.per-file-ignores]
"docs/conf.py" = ["D100", "INP001"]
"tests/*" = [
"ARG001", "D", "FBT003", "INP001", "PLR2004", "S", "TRY003",
]
With this starting point, check which rules fail:
ruff check . --statistics
And check individual failures, for example:
ruff check . --select D400
As general guidance:
Fix failures, if possible.
Use a
# noqa: RULE
comment if the failure is rare (for example, anS
rule), or if it should be fixed, given more time. Add a short comment to explain the failure. For example:# noqa: S104 # Docker
Use
per-file-ignores
if the failures occur in a single (or a set of) files. For example:"*/__main__.py" = ["T201"] # print
Use
ignore
if the failures occur in disparate files and are expected to occur in new code. For example:"TRY003", # errors
Use settings where possible, instead of ignoring rules entirely. Notably, use:
builtins-ignorelist, instead of A002
extend-immutable-calls, instead of B008
allowed-confusables, instead of RUF001
extend-ignore-names, instead of SLF001
isort:skip
and type: ignore
comments should be avoided, and should reference the specific error if used, to avoid shadowing another error: for example, # type: ignore[attr-defined]
.
Complexity rules
We ignore the C901
and all PLR091
rules.
Complexity is best measured by the effort required to read and modify code. This cannot be measured using techniques like cyclomatic complexity. Reducing cyclomatic complexity typically means extracting single-caller methods and/or using object-oriented programming, which frequently increases cognitive complexity.
See the note under Create products sustainably.
Pre-commit hooks¶
To avoid pushing commits that fail formatting or linting checks, new projects should use pre-commit. For example, if Ruff is configured as above, create a .pre-commit-config.yaml
file:
ci:
autoupdate_schedule: quarterly
skip: [pip-compile]
default_language_version:
python: python3.11
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.3
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.4.4
hooks:
- id: pip-compile
name: pip-compile requirements.in
args: [requirements.in, -o, requirements.txt]
- id: pip-compile
name: pip-compile requirements_dev.in
args: [requirements_dev.in, -o, requirements_dev.txt]
files: ^requirements(_dev)?\.(in|txt)$
ci:
autoupdate_schedule: quarterly
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.3
hooks:
- id: ruff
- id: ruff-format
Note
Applications set the correct Python version in the default_language_version section. Otherwise, pre-commit.ci (or the pre-commit
command locally) can use the incorrect Python version for the pip-compile
hook.
pre-commit.ci disallows network connections. As such, the pip-compile
hook is configured to be skipped in the ci
section, and is run by the lint.yml workflow, instead.
Note
pre-commit/pre-commit-hooks is not used in the templates, as the errors it covers are rarely encountered.
Tip
If you encounter an error like:
RuntimeError: failed to find interpreter for Builtin discover of python_spec='python3.10'
pre-commit uses virtualenv to discover Python interpreters. On macOS, install the missing version with Homebrew, instead of uv:
brew install python@3.10
Continuous integration¶
Create a .github/workflows/lint.yml
file.
The Django and Pypackage Cookiecutter templates contain default workflows.
See also
Workflow files for linting shell scripts and Javascript files
standard-maintenance-scripts to learn about the Bash scripts
Additional linting¶
codespell finds typographical errors. It is especially useful in repositories with lengthy documentation. Otherwise, all repositories can be periodically checked with:
codespell -S '.git,.pytest_cache,cassettes,fixtures,_build,build,dist,target,locale,locales,vendor,node_modules,docson,htmlcov,schemaspy,*.csv,*.json,*.jsonl,*.map,*.po,european-union-support'