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, an S 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:

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

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'