pre-commit your Django projects

Improve your code quality by using simple scripts

Pre-commit is a python based tool to enable easy integration of git hooks, and it's supported by plenty of tools like pyliny, flake8, 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.3.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

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.10.1
    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=310
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: v2.38.0
    hooks:
      - id: pyupgrade
        args: ["--py3-plus", "--py39-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.10.0"
    hooks:
      - id: django-upgrade
        args: [--target-version, "4.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.4
    hooks:
      - id: bandit
        args: ["-iii", "-ll"]

Flake8

Flake8 is a python tool that glues together pycodestyle, pyflakes, mccabe, and third-party plugins to check the style and quality of some python code.

- repo: https://github.com/PyCQA/flake8
    rev: 5.0.4
    hooks:
      - id: flake8
        exclude: ^(.+)\/migrations\/(.+)$
        # Extra flake8 checks
        additional_dependencies: [
            "flake8-bugbear",
            "flake8-comprehensions",
            "flake8-mutable",
            "flake8-print",
            "flake8-simplify",
            "flake8-django",
          ]

Final result

The final .pre-commit-config.yaml

repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.3.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.10.1
    hooks:
      - id: isort
- repo: https://github.com/asottile/pyupgrade
    rev: v2.38.0
    hooks:
      - id: pyupgrade
        args: ["--py3-plus", "--py39-plus"]
- repo: https://github.com/adamchainz/django-upgrade
    rev: "1.10.0"
    hooks:
      - id: django-upgrade
        args: [--target-version, "4.0"]
- repo: https://github.com/pycqa/bandit
    rev: 1.7.4
    hooks:
      - id: bandit
        args: ["-iii", "-ll"]
- repo: https://github.com/PyCQA/flake8
    rev: 5.0.4
    hooks:
      - id: flake8
        exclude: ^(.+)\/migrations\/(.+)$
        # Extra flake8 checks
        additional_dependencies: [
            "flake8-bugbear",
            "flake8-comprehensions",
            "flake8-mutable",
            "flake8-print",
            "flake8-simplify",
            "flake8-django",
          ]

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

Example with 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/