Django Code Formatting and Linting Made Easy: A Step-by-Step Pre-commit Hook Tutorial
Hana Belay
Posted on October 5, 2023
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
- Prerequisite
- Configure a Django Project
- Introduction to Pre-commit
- Configure Black
- Configure Isort
- Configure Flake8
- Bonus: Configure Djlint
- Running Pre-commit in Continuous Integration (CI)
- 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
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
Next, create a file named .pre-commit-config.yaml
at the root of the project.
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:
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:
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:
pre-commit run --all-files
As you can see in the image below, the check-yaml
script ran successfully while the other 2 failed.
The hooks will automatically fix the issues so that’s great. Now, we can stage the changed files and commit them:
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\/.*
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
-
rev
The latest version of black at the time of writing is23.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
Open the file and add the following settings:
[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:
pre-commit run --all-files
A lot of things have been automatically reformatted as you can see:
Now, I am going to stage the changes and 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)
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"]
-
profile
is built into isort to allow easy interoperability with common projects and code styles. -
combine_as_imports
Combinesas
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 howfrom
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
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.
Stage and commit changes:
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
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:
touch .flake8
Then, add the following setting in the file:
[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:
pre-commit run --all-files
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
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
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
Now let’s run pre-commit again:
pre-commit run --all-files
Voila, everything is fixed, stage and commit the changes:
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
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"
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:
- Go to https://pre-commit.ci/
- Press sign in with GitHub
- Install this service into a GitHub repository of your choice
That’s it!
This is pre-commit running after making a PR:
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! 🖤
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
October 5, 2023