Django Code Formatting and Linting Made Easy: A Step-by-Step Pre-commit Hook Tutorial
Tags: python, django, webdev, git
Published: 2023-10-05 06:41:11

When it comes to coding, we should all aspire to adhere to best practices and industry standards to foster easy communication and collaboration. Formatting and Linting tools allow you to improve your code quality against a set of pre-defined standards.

**Formatting:** What could be more delightful than code that reads like a well-crafted story, consistently styled and structured? Formatting helps you refine your code layout, indentation, styling, and spacing, enhancing not only its aesthetics but also its readability and reusability.

**Linting:** Beyond the basic syntax rules, linting allows you to keep your code healthy by checking it for potential bugs, and deviations from established best practices. It also points out areas of improvement and guides you towards creating cleaner, more robust code.

In a team of developers, it is significant to ensure that every team member contributes code that communicates effectively with each other and remains comprehensible to any future developer.

In this blog post, we will set up formatting and linting in a Django project. We will try to incorporate some of the coding styles recommended in Django’s [documentation](https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/) by using tools like pre-commit, black, isort, and flake8. We will also learn how to use pre-commit in continuous integration (CI).

## Table of Contents

1. [Prerequisite](#prerequisite)
2. [Configure a Django Project](#configuration)
3. [Introduction to Pre-commit](#pre-commit)
4. [Configure Black](#black)
5. [Configure Isort](#isort)
6. [Configure Flake8](#flake8)
7. [Bonus: Configure Djlint](#djlint)
8. [Running Pre-commit in Continuous Integration (CI)](#ci)
9. [Conclusion](#conclusion)

## Prerequisite <a name="prerequisite"></a>

This guide assumes that you are familiar with Django, Git, and GitHub.

## Configure a Django Project <a name="configuration"></a>

The Django project I am going to use is one of my own which is an [e-commerce API](https://github.com/earthcomfy/django-ecommerce-api.git) built using Django Rest Framework. I will demonstrate the concepts by using this project so you can clone it to your local machine. However, feel free to use alternative projects if you prefer.

```bash
git clone https://github.com/earthcomfy/django-ecommerce-api.git
git checkout without-pre-commit
```

You can then create/activate a virtual environment and follow along.

## Introduction to Pre-commit <a name="pre-commit"></a>

Git hooks are scripts that Git executes before or after events such as committing, merging, or pushing. These scripts allow you to customize and automate parts of your Git workflow. 

Pre-commit is a type of Git hook that runs just before you make a commit. When you run `git commit`, the pre-commit hook checks whether or not the changes being committed meet certain standards that are configured. These standards are commonly linters and code formatters. If your changes don’t meet those rules and standards, your commit will be rejected. In some cases, the linter and formatter will automatically fix the issues on the spot. In other cases, however, the linter will tell you what needs to be fixed and where.

[Pre-commit](https://pre-commit.com/) is a framework for managing and maintaining multi-language pre-commit hooks. It supports hooks for various programming languages. Using this framework, you only have to specify a list of hooks you want to run before every commit, and pre-commit handles the installation and execution of those hooks despite your project’s primary language.

That being said, let’s now install this package in our project:

```bash
pip install pre-commit
```

Next, create a file named `.pre-commit-config.yaml` at the root of the project.

```bash
touch .pre-commit-config.yaml
```

The heart of the `pre-commit` framework is the `.pre-commit-config.yaml` file. This file defines the hooks you want to run and where to find them. Hooks are specified as Git repositories, and each hook is associated with a command or script to execute.

Now, let’s add our first hook:

```yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
      - id: check-yaml
      - id: end-of-file-fixer
      - id: trailing-whitespace
```

The above configuration uses hooks from the `pre-commit/pre-commit-hooks` repository. The latest version at the time of writing is `4.4.0` You can check for the latest releases in the repository of any hook you want to add. The above configuration specifies 3 scripts that will be run:

- `check-yaml` Attempts to load all yaml files to verify syntax.
- `end-of-file-fixer` Makes sure files end in a new line and only a new line.
- `trailing-whitespace` Trims trailing whitespace.

For a list of other hooks provided by this repository, check https://github.com/pre-commit/pre-commit-hooks

Once the configuration is set up, run:

```bash
pre-commit install
```

This will set up the hooks locally based on the configuration specified in `.pre-commit-config.yaml` file. After this installation, every time you run `git commit`, the configured hooks will automatically run on the staged files.

Note that you can also run `pre-commit run --all-files` at any time to run all configured hooks on all files in your project, rather than just the files that are staged for the next commit. This command is especially helpful during the initial setup of `pre-commit` to check all files and identify and fix issues before making the initial commit.

So, let’s do exactly that:

```python
pre-commit run --all-files
```

As you can see in the image below, the `check-yaml` script ran successfully while the other 2 failed.

![pre-commit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fi8q9omjn59qaw5m71h9.png)

The hooks will automatically fix the issues so that’s great. Now, we can stage the changed files and commit them:


![pre-commit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yuaxq3f5bdytlwo4in3c.png)

Commit successful 🙂

Depending on your project, you can set up as many hooks as you like. In our case, we are going to set up 3 hooks: black, isort, flake8. We will also set up djlint as a bonus.

## Configure Black <a name="black"></a>

[Black](https://black.readthedocs.io/en/stable/) is a Python code formatter that automatically formats Python code to comply with its style guide called PEP 8. PEP 8 is the official style guide for Python code, and it provides recommendations on how to format code for better readability and consistency.

Pre-commit will not run on gitignored files. In a Django project, we usually track migration files (unless you have a specific reason not to). Given that migration files are auto-generated by Django, it’s better not to run pre-commit on these files. Therefore, before configuring Black, let’s exclude migration files. At the top of the config file, add the following:

```yaml
exclude: .*migrations\/.*
```

Then let’s configure the hook:

```yaml
- repo: https://github.com/psf/black-pre-commit-mirror
    rev: 23.9.1
    hooks:
      - id: black
        language_version: python3.10
```

- `rev` The latest version of black at the time of writing is `23.9.1`
- `language_version` Black recommends specifying the latest version of Python supported by your project.

The next step is to customize settings for black. We are going to do this customization in a file called `pyproject.toml`. 

`pyproject.toml` is a configuration file used in Python projects. It is designed to specify project information and configuration details in a standardized way. The advantage of using this config file is that it is quickly becoming a standard place to configure most Python tools such as black and Isort.

Let’s create the file at the root of the project:

```bash
touch pyproject.toml
```

Open the file and add the following settings:

```toml
[tool.black]
line-length = 88
target-version = ['py310']
include = '\.pyi?$'
```

- `line-length` How many characters per line to allow. The default is 88.
- `target-version` Python versions that should be supported by Black’s output.

For a list of other configurations, check https://black.readthedocs.io/en/stable/usage_and_configuration/

Pre-commit will pick up these configs and check your project against them.

As mentioned before, it is a good practice to run all files after adding a new hook:

```bash
pre-commit run --all-files
```

A lot of things have been automatically reformatted as you can see:

![pre-commit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x7ng3v1slza60jwepusg.png)

Now, I am going to stage the changes and commit:

![pre-commit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ytl4nuxhd1nyl0r9tmgf.png)

## Configure Isort <a name="isort"></a>

[isort](https://pycqa.github.io/isort/) is a Python utility that helps in sorting and organizing import statements in Python code to create readable and consistent code. It automatically formats import statements in accordance with PEP 8.

Add the hook to pre commit config file:

```yaml
- repo: https://github.com/pycqa/isort
    rev: 5.12.0
    hooks:
      - id: isort
        name: isort (python)
```

Similar to Black, you can tailor the settings for isort by utilizing the same `pyproject.toml` file. Append the following configurations for isort immediately below the existing black settings:

```toml
[tool.isort]
profile = "django"
combine_as_imports = true
include_trailing_comma = true
line_length = 88
multi_line_output = 3
known_first_party = ["config"]
```

- `profile` is built into isort to allow easy interoperability with common projects and code styles.
- `combine_as_imports` Combines `as` imports on the same line.
- `include_trailing_comma` Includes a trailing comma on multi-line imports that include parentheses.
- `line_length` The max length of an import line (used for wrapping long imports). It should be the same as black.
- `multi_line_output` Defines how `from` imports wrap when they extend past the line_length limit.
- `known_first_party` Force isort to recognize a module as being part of the current Python project. It should be the name of your Django project.

For a list of other configuration options, check https://pycqa.github.io/isort/docs/configuration/options.html

let’s now run pre-commit against all files:

```bash
pre-commit run --all-files
```

You can see a lot of files have been fixed by isort. Also, the pre-commit config file has been updated to trim a trailing whitespace.


![pre-commit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rapx8y2cqccc18mhz6be.png)

Stage and commit changes:

![pre-commit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lexrbevudxqpn0aob6cj.png)

## Configure Flake8 <a name="flake8"></a>

[Flake8](https://flake8.pycqa.org/en/latest/) is a popular linting tool for Python code. It is not a single tool but a wrapper that integrates several other tools to check your code for compliance with style guides such as PEP 8 and to detect various programming errors.

To configure the hook, append the following in pre-commit config file:

```yaml
- repo: https://github.com/pycqa/flake8
    rev: 6.1.0
    hooks:
      - id: flake8
```

Flake8 supports storing its configuration in your project in one of `setup.cfg`, `tox.ini`, or `.flake8` file.

Let’s go with `.flake8` option. Create a `.flake8` file at the root of your project:

```bash
touch .flake8
```

Then, add the following setting in the file:

```bash
[flake8]
max-line-length = 88
```

For a list of configuration options, check https://flake8.pycqa.org/en/latest/user/configuration.html

Let’s now run pre-commit against all files:

```bash
pre-commit run --all-files
```

![pre-commit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sa8e3zhfpf606ezp4mdy.png)

Note that this plugin doesn't automatically fix the issues for us. It only tells us what is wrong and where. Let’s discuss the issues raised by flake8:

1) The first issue arises from the settings. In the `base.py` file, the `os` module is imported, although it is not directly utilized within that file. Instead, it is used in the `development.py` which is imported from the base. To address this issue, we can eliminate the import statement from the `base.py` file and explicitly incorporate it in the `development.py` file.
2) For the line too long issue in the `base.py` file, I am going to ignore it and put `# noqa` 
3) The `unable to detect undefined names` stems from the fact that we are using global imports in the settings. This specific case related to the settings is fine by me so I am going to tell `flake8` to ignore this issue. Go to the `.flak8` file and append this:

```bash
per-file-ignores =
    config/settings/*:F405 F403 F401
```

4) The line-length violations (E501) are caused because Black doesn't know how to operate on strings. So I am actually going to ignore this warning. I am also going to ignore W503, pertaining to the line break before a binary operator, as part of this approach. Add this in `.flake8` file:

```bash
ignore = E501, W503
```

The other issues F401, E712, and F841 have been fixed. This is what the final `.flake8` file looks like:

```bash
[flake8]
max-line-length = 88
ignore = E501, W503
per-file-ignores =
    config/settings/*:F405 F403 F401
```

Now let’s run pre-commit again:

```bash
pre-commit run --all-files
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zz9b1l6b6qy5ze5u2u04.png)

Voila, everything is fixed, stage and commit the changes:

![pre-commit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z2c6qh0wa7a5feg6hy0v.png)

# Bonus Tool: Djlint <a name="djlint"></a>

[Djlint](https://www.djlint.com/) is a valuable tool designed for formatting and linting Django HTML templates. I have extensively searched for such a tool, especially because Prettier does not support Django HTML files, making Djlint a good alternative.

Although our current Django project doesn't involve HTML files, it's straightforward to set up Djlint, and the configuration process aligns with the approach we've taken for other hooks.

Add the following to the pre-commit config file:

```yaml
- repo: https://github.com/Riverside-Healthcare/djLint
    rev: v1.32.0
    hooks:
      - id: djlint-reformat-django
      - id: djlint-django
```

To customize Djlint settings, similar to what we did for Black and isort, you can utilize the `pyproject.toml` file:

```toml
[tool.djlint]
profile = "django"
ignore = "H031"
```

You can ignore linting rules as per your requirements. For example, in the above config, H031 which checks for the presence of meta keywords is ignored. You can check https://www.djlint.com/docs/linter/ for a list of linting rules.

## Running Pre-Commit in Continuous Integration (CI) <a name="ci"></a>

Running pre-commit in Continuous Integration (CI) is a practice that involves integrating pre-commit checks into the CI/CD (Continuous Integration/Continuous Deployment) pipeline. By incorporating formatting and linting checks into the CI process, you ensure that code quality standards are maintained and potential issues are caught early in the development life-cycle.

Welcome [pre-commit ci](https://pre-commit.ci/), a continuous integration service for the pre-commit framework.

To configure the service, follow the steps below:

1. Go to https://pre-commit.ci/
2. Press sign in with GitHub

![pre-commit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lrs6kdoxxp6uqh1gojuk.png)

1. Install this service into a GitHub repository of your choice

![pre-commit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kcqqe8z3b3ak1skyj38p.png)

That’s it!

This is pre-commit running after making a PR:

![pre-commit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/62a9xceoyxgcmw5ihegy.png)

![pre-commit](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dtpw57nfk14fel1c483n.png)

## Conclusion <a name="conclusion"></a>

I hope you found this article helpful. By automating formatting and enforcing linting standards, you can focus on business logic and architecture and save yourself time. If you got lost somewhere throughout the guide, check out the project on [GitHub](https://github.com/earthcomfy/django-ecommerce-api)

Happy coding! 🖤