pre-commit your Django projects

Improve your code quality by using simple scripts

Updated 08/02/2024: Updated example dependencies, use Ruff instead of flake8 and pylint

Pre-commit is a python based tool to enable easy integration of git hooks, and it's supported by plenty of tools like ruff, black, ...

While coding a Django project in a team, you would like to have unified rules and styles to avoid unnecessary changes on each Pull Request another team member will review. After setting up for the first time, this is for me a must for every project to ensure consistency and code quality.

After installing pre-commit, you will need to create a .pre-commit-config.yaml file and run pre-commit install inside the project to activate the git hooks.

Default pre-commit hooks

repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
    -   id: check-yaml
    -   id: end-of-file-fixer
    -   id: trailing-whitespace
-   repo: https://github.com/psf/black
    rev: 24.1.1
    hooks:
    -   id: black

Check this link for a full list of builtins hooks from pre-commit.

isort

The initial example contains configuration of python Black formatting, on top of it I strongly recommend using consistent sorted import via isort

  - repo: https://github.com/pycqa/isort
    rev: 5.13.2
    hooks:
      - id: isort

If you use isort with black you will need to use black profile as follow in pyproject.toml in your root project directory.

[tool.black]
line-length = 88
include = '\.pyi?$'

[tool.isort]
profile = "black"
py_version=311
multi_line_output = 3
line_length = 88
default_section = "THIRDPARTY"
skip = ["migrations"]

Pyupgrade

Another amazing tool you might need to use is pyupgrade, this one will ensure you will use new python feature like new style classes, removing unicode literals, use the python3 super() calls etc.

- repo: https://github.com/asottile/pyupgrade
    rev: v3.15.0
    hooks:
      - id: pyupgrade
        args: ["--py3-plus", "--py311-plus"]

If you use python 3.10 you can change --py39-plus with --py310plus

Djangoupgrade

Next similar tools is djangoupgrade, it will fix deprecated features of Django and make it easy to upgrade your Django projects, unfortunately is does only check against python files, for Django templates it's not doing anything, but I still find it useful.

- repo: https://github.com/adamchainz/django-upgrade
    rev: "1.15.0"
    hooks:
      - id: django-upgrade
        args: [--target-version, "5.0"]

Note the target-version, this depend on the current supported Django version in the project.

Bandit

Another great tool to use is Bandit, Bandit is designed to find common security issues in Python code. To do this Bandit processes each file, builds an AST from it, and runs appropriate plugins against the AST nodes. Once Bandit has finished scanning all the files it generates a report.

- repo: https://github.com/pycqa/bandit
    rev: 1.7.7
    hooks:
      - id: bandit
        args: ["-iii", "-ll"]

Ruff

Ruff is a powerful Python tool that consolidates various code quality checkers like pycodestyle, pyflakes, and mccabe, along with third-party plugins. It ensures the style and quality of Python code through a unified interface.

- repo: https://github.com/charliermarsh/ruff-pre-commit
    # Ruff version.
    rev: "v0.2.1"
    hooks:
      - id: ruff

Now we will need to setup Ruff config in pyproject.toml

[tool.ruff]
line-length = 88
target-version = "py311"
exclude = [
    ".git",
    ".mypy_cache",
    ".pre-commit-cache",
    ".ruff_cache",
    ".tox",
    ".venv",
    "venv",
    "docs",
    "__pycache",
    "**/migrations/*",
]
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

[tool.ruff.lint.mccabe]
max-complexity = 10

To run ruff manually, once installed (e.g pip install ruff)

ruff check .

Final result

The final .pre-commit-config.yaml

repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
    -   id: check-yaml
    -   id: end-of-file-fixer
    -   id: trailing-whitespace
-   repo: https://github.com/psf/black
    rev: 22.8.0
    hooks:
    -   id: black
- repo: https://github.com/pycqa/isort
    rev: 5.13.2
    hooks:
      - id: isort
- repo: https://github.com/asottile/pyupgrade
    rev: v3.15.0
    hooks:
      - id: pyupgrade
        args: ["--py3-plus", "--py39-plus"]
- repo: https://github.com/adamchainz/django-upgrade
    rev: "1.15.0"
    hooks:
      - id: django-upgrade
        args: [--target-version, "4.0"]
- repo: https://github.com/pycqa/bandit
    rev: 1.7.7
    hooks:
      - id: bandit
        args: ["-iii", "-ll"]
- repo: https://github.com/charliermarsh/ruff-pre-commit
    # Ruff version.
    rev: "v0.2.1"
    hooks:
      - id: ruff

Bonus: CI integration

To make sure no one skip the pre-commit setup, either by intention of if someone forget to install the hooks locally after cloning the project for the first time, you can run pre-commit run --all in your CI.

Caching might be tricky and useful as initial environment setup take some time and therefore you can use the PRE_COMMIT_HOME for specifying which directory is used for the cache

Gitlab CI

precommit:
  image:
    name: python:3.10
  stage: test
  variables:
    PRE_COMMIT_HOME: ".cache/.pre-commit-cache/"
  script:
    - pip install pre-commit
    - pre-commit run --all
  rules:
    - exists:
        - ".pre-commit-config.yaml"
  interruptible: true
  cache:
    key: $CI_PROJECT_NAME-precommit-cache-v1
    paths:
      - .cache/.pre-commit-cache/

Github Actions

name: CI

on:
  create:
    tags:
      - "*" # run on all tags
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: 3.11
      - name: Install pre-commit
        run: pip install pre-commit
      - name: Cache pre-commit hooks
        id: pre-commit-cache
        uses: actions/cache@v2
        with:
          path: $HOME/.cache/pre-commit
          key: ${{ runner.os }}-pre-commit-hooks-${{ hashFiles('**/.pre-commit-config.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pre-commit-hooks-
      - name: Run pre-commit
        run: |
          pre-commit run --all-files