pre-commit with Django
Updated
Table of Contents
pre-commit is a widely-used code quality framework. It allows a developer to add hooks for various code quality tools that check your code for any errors or issues before committing them to a repository. Django adopted pre-commit
in 2020, and for many developers, it is an indispensable tool used on basically every project.
In this tutorial, we will create a new Django project, install pre-commit,
and configure three of the more popular Django-related hooks:
- Black, the Python code formatter
- isort, to sort and group imports properly
- ruff, a Python linter written in Rust
Django Set Up
Let's go through the standard installation for a new Django project. On the command line, navigate to a new directory, create a new virtual environment, and install Django.
# Windows
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $ python -m pip install django~=5.0.0
# macOS
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ python3 -m pip install django~=5.0.0
Create a new project called django_project
and then run runserver
to start up the local web server and view the Django welcome page in your web browser at 127.0.0.1:8000
.
(.venv) $ django-admin startproject django_project .
(.venv) $ python manage.py runserver
Install pre-commit
There are multiple ways to install pre-commit. In this tutorial, we will use pip,
but some developers on macOS prefer installing it via Homebrew and updating it that way.
(.venv) $ python -m pip install pre-commit
To confirm it installed correctly, check its version:
(.venv) $ pre-commit version
pre-commit 3.3.3
Add a Configuration File
pre-commit
is configured via a .pre-commit-config.yaml
file located in the root of your directory. Create this new file now with your text editor.
├── .pre-commit-config.yaml # new
├── db.sqlite3
├── django_project
│ ├── ...
└── manage.py
There is a helpful command to output a sample configuration.
(.venv) $ pre-commit sample-config
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
Briefly, this configuration references the source repository, version number, and contains four hooks that respectively remove whitespace, make sure text files end with a newline, check that YAML files are valid, and prevent large files (the default is 500kB) from being committed.
Copy and paste this into the .pre-commit-config.yaml
file.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
It is also possible--and highly recommended--to specify which version of Python pre-commit
should be used to run hooks. This can be done via the default_language_version
top-level key. We are using Python 3.11 here.
# .pre-commit-config.yaml
default_language_version:
python: python3.11
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
Git setup
Make sure that git is already installed since we can't use pre-commit without git itself.
Confirm that git is installed by checking its version number.
(.venv) $ git --version
git version 2.39.2 (Apple Git-143)
Then add a .gitignore
file to the root directory and include one line excluding the virtual environment directory, .venv
.
# .gitignore
.venv
We can now initialize a new repository and add all changed files, but do not commit it yet.
(.venv) $ git init
(.venv) $ git add .
Install git Hook Scripts
Run the pre-commit install command to set up the git hook scripts in our repository.
(.venv) $ pre-commit install
pre-commit installed at .git/hooks/pre-commit
pre-commit
will now run automatically on any future git commit
command!
When adding new hooks, it is a good idea to run them against all files since usually, pre-commit
only runs on changed files during git hooks.
(.venv)
$ pre-commit run --all-files
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook
Fixing .gitignore
Check Yaml...............................................................Passed
Check for added large files..............................................Passed
What's this? Our pre-commit
hook noticed an error already: we failed to add an extra line to the end of the .gitignore
file, so it flagged it as "failed" and then fixed it automatically. If you look at the .gitignore
file, you'll see a second line now.
# .gitignore
.venv
Everything will pass if you run pre-commit run --all-files
again.
(.venv) $ pre-commit run --all-files
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Check for added large files..............................................Passed
It's time to stage the changes to the .gitignore
file and make our first git commit.
(.venv) $ git add .
(.venv) $ git commit -m "initial commit"
Add hooks: Black
Black is a popular Python code formatter that checks all code against the PEP 8 standard. Django has formally adopted it per DEP 8. We can add it to our .pre-commit-config.yaml
file as a hook.
# .pre-commit-config.yaml
default_language_version:
python: python3.11
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black
Make sure to include that extra line at the end of the file, or pre-commit
will complain. Now add our changes to git. Since we are adding a new hook, it is necessary to run it initially against all files.
(.venv) $ git add .
(.venv) $ pre-commit run --all-files
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
check yaml...............................................................Passed
check for added large files..............................................Passed
black....................................................................Passed
Everything passed, so we can make a git commit with a message about adding a hook for Black.
(.venv) $ git commit -m "add Black hook"
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
check yaml...............................................................Passed
check for added large files..............................................Passed
black................................................(no files to check)Skipped
[main b29d356] add black
1 file changed, 4 insertions(+), 4 deletions(-)
Notice that Black "Skipped" running because the only change was to the .pre-commit-config.yaml
file, and Black only applies to Python files.
pyproject.toml
A pyproject.toml
file acts as a unified Python project settings file. Create a new blank file in your root project directory.
├── .pre-commit-config.yaml
├── db.sqlite3
├── django_project
│ ├── ...
├── manage.py
└── pyproject.toml # new
We can use it to add another layer of configuration to our hooks. First, set a target-version
for Python 3.11 within the pyproject.toml
file. We can also specify a Python version for our Black hook.
# pyproject.toml
target-version = "py311"
[tool.black]
target-version = ["py311"]
Now git add
the new pyproject.toml
file and create a git commit.
(.venv) $ git add .
(.venv) $ git commit -m "add pyproject.toml file"
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
check yaml...........................................(no files to check)Skipped
check for added large files..............................................Passed
black................................................(no files to check)Skipped
[main d247439] pyproject.toml
1 file changed, 4 insertions(+)
create mode 100644 pyproject.toml
Add hooks: isort
isort is a code formatter that sorts imports automatically. It is a very popular package and has been used by Django since 2015. To add it as a hook, update the .pre-commit-config.yaml
file as follows:
# .pre-commit-config.yaml
default_language_version:
python: python3.11
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
Then run the new hook against all files.
(.venv) $ pre-commit run --all-files
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
check yaml...............................................................Passed
check for added large files..............................................Passed
black....................................................................Passed
isort....................................................................Passed
Now it can be added to git and committed with a message.
(.venv) $ git add .
(.venv) $ git commit -m "add isort hook"
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
check yaml...............................................................Passed
check for added large files..............................................Passed
black................................................(no files to check)Skipped
isort................................................(no files to check)Skipped
[main 8af5fa2] add isort hook
1 file changed, 4 insertions(+)
Add hooks: ruff
Historically, flake8 was the Python linter of choice, but recently ruff has emerged, an alternative written in Rust and promising much faster speeds. I will avoid any deep discussion of flake8
vs ruff
other than to say I appreciate both tools but will focus on ruff
here. You can see ruff's take on compatability with Black in the docs.
Add ruff
as follows to the pre-commit
config file.
# .pre-commit-config.yaml
default_language_version:
python: python3.11
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black
alias: autoformat
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.285
hooks:
- id: ruff
alias: autoformat
args: [--fix]
Then in pyproject.toml
, we'll add some additional configuration to enable Pyflakes by default and ignore errors around line lengths (E501) and ambiguous file names (E741).
# pyproject.toml
target-version = "py311"
[tool.black]
target-version = ["py311"]
[tool.ruff]
# Enable Pyflakes `E` and `F` codes by default.
select = ["E", "F"]
ignore = ["E501", "E741"]
Run ruff
against all files now.
(.venv) $ pre-commit run --all-files
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
check yaml...............................................................Passed
check for added large files..............................................Passed
black....................................................................Passed
isort....................................................................Passed
ruff.....................................................................Passed
Then we can git add
changes to the .pre-commit-config.yaml
and pyproject.toml
and make a git commit.
(.venv) $ git add .
(.venv) $ git commit -m "add ruff"
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
check yaml...............................................................Passed
check for added large files..............................................Passed
black................................................(no files to check)Skipped
isort................................................(no files to check)Skipped
ruff.................................................(no files to check)Skipped
[main 979212f] add ruff
2 files changed, 12 insertions(+)
Next Steps
We've only scratched the surface of what pre-commit
and all the various hooks can do. Some other popular options include pyupgrade, django-upgrade, and bandit.
If you're looking for a detailed discussion of pre-commit
as well as tools and techniques to improve your Django development experience, I highly recommend Adam Johnson's book on the subject, Boost Your Django DX.