Django Code Formatting and Linting Made Easy: A Step-by-Step Pre-commit Hook Tutorial

earthcomfy

Hana Belay

Posted on October 5, 2023

Django Code Formatting and Linting Made Easy: A Step-by-Step Pre-commit Hook Tutorial

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 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
  2. Configure a Django Project
  3. Introduction to Pre-commit
  4. Configure Black
  5. Configure Isort
  6. Configure Flake8
  7. Bonus: Configure Djlint
  8. Running Pre-commit in Continuous Integration (CI)
  9. Conclusion

Prerequisite

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

Configure a Django Project

The Django project I am going to use is one of my own which is an e-commerce API 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.

git clone https://github.com/earthcomfy/django-ecommerce-api.git
git checkout without-pre-commit
Enter fullscreen mode Exit fullscreen mode

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

Introduction to Pre-commit

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 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:

pip install pre-commit
Enter fullscreen mode Exit fullscreen mode

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

touch .pre-commit-config.yaml
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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:

pre-commit install
Enter fullscreen mode Exit fullscreen mode

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:

pre-commit run --all-files
Enter fullscreen mode Exit fullscreen mode

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

pre-commit

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

pre-commit

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

Black 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:

exclude: .*migrations\/.*
Enter fullscreen mode Exit fullscreen mode

Then let’s configure the hook:

- repo: https://github.com/psf/black-pre-commit-mirror
    rev: 23.9.1
    hooks:
      - id: black
        language_version: python3.10
Enter fullscreen mode Exit fullscreen mode
  • 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:

touch pyproject.toml
Enter fullscreen mode Exit fullscreen mode

Open the file and add the following settings:

[tool.black]
line-length = 88
target-version = ['py310']
include = '\.pyi?$'
Enter fullscreen mode Exit fullscreen mode
  • 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:

pre-commit run --all-files
Enter fullscreen mode Exit fullscreen mode

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

pre-commit

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

pre-commit

Configure Isort

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:

- repo: https://github.com/pycqa/isort
    rev: 5.12.0
    hooks:
      - id: isort
        name: isort (python)
Enter fullscreen mode Exit fullscreen mode

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:

[tool.isort]
profile = "django"
combine_as_imports = true
include_trailing_comma = true
line_length = 88
multi_line_output = 3
known_first_party = ["config"]
Enter fullscreen mode Exit fullscreen mode
  • 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:

pre-commit run --all-files
Enter fullscreen mode Exit fullscreen mode

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

Stage and commit changes:

pre-commit

Configure Flake8

Flake8 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:

- repo: https://github.com/pycqa/flake8
    rev: 6.1.0
    hooks:
      - id: flake8
Enter fullscreen mode Exit fullscreen mode

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

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

touch .flake8
Enter fullscreen mode Exit fullscreen mode

Then, add the following setting in the file:

[flake8]
max-line-length = 88
Enter fullscreen mode Exit fullscreen mode

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:

pre-commit run --all-files
Enter fullscreen mode Exit fullscreen mode

pre-commit

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:

per-file-ignores =
    config/settings/*:F405 F403 F401
Enter fullscreen mode Exit fullscreen mode

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:

ignore = E501, W503
Enter fullscreen mode Exit fullscreen mode

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

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

Now let’s run pre-commit again:

pre-commit run --all-files
Enter fullscreen mode Exit fullscreen mode

Image description

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

pre-commit

Bonus Tool: Djlint

Djlint 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:

- repo: https://github.com/Riverside-Healthcare/djLint
    rev: v1.32.0
    hooks:
      - id: djlint-reformat-django
      - id: djlint-django
Enter fullscreen mode Exit fullscreen mode

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

[tool.djlint]
profile = "django"
ignore = "H031"
Enter fullscreen mode Exit fullscreen mode

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)

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, 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

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

pre-commit

That’s it!

This is pre-commit running after making a PR:

pre-commit

pre-commit

Conclusion

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

Happy coding! 🖤

💖 💪 🙅 🚩
earthcomfy
Hana Belay

Posted on October 5, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related