48
Git Pre-commit Hooks for Automatic Python Code Formatting
Python has become the world’s most popular programming language because of its elegant syntax. But this alone doesn’t ensure a clean, readable code.
The Python community had evolved to create standards to make codebases created by different programmers look as if the same person had developed them. Later on, packages such as Black were created to auto-format the codebase. Yet the problem is only half solved. Git pre-commit hooks complete the rest.
What are pre-commit hooks?
Pre-commit hooks are helpful git scripts that run automatically before git commit. If a pre-commit hook fails, the git push will be aborted, and depending on how you set it up, your CI software may also fail or not trigger at all.
Note, before setting up pre-commit hooks, ensure you have the following.
- You need to have git version >= v.0.99 (you can check this with git — version)
- You need to have Python installed (as it’s the language used for git hooks.)
You can install the pre-commit package easily with single pip command. But to attach it to your project, you need one more file.
pip install pre-commit
The .pre-commit-config.yaml
file holds all the configurations your project requires. This is where you tell pre-commit what actions it needs to perform before every commit and override their defaults if needed.
The following command will generate an example configuration file.
pre-commit sample-config > .pre-commit-config.yaml
Here is an example configuration that sorts your requirements.txt file before every time you commit your changes. Place this at the root of your project directory or edit the one you generated using the previous step.
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: requirements-txt-fixer
We’ve installed the pre-commit package, and we configured how it should work. Now, we can enable pre-commit hooks to this repository using the following command.
Awesome! Now add the following requirements.txt file to your project and make a commit. See pre-commit hooks in action.
# requirement.txt before commit.
urllib3==1.26.7
openpyxl==3.0.9
pandas==1.3.3
Before committing, git will download instructions from the repository and use the requirement fixer module to clean the file. The resulting file will look like the below.
# requirement.txt after commit.
openpyxl==3.0.9
pandas==1.3.3
urllib3==1.26.7
Most code editors have keyboard shortcuts that you can bind to Black so that you can clean your code on the go. For example, VSCode on Linux uses Ctrl + Shift + I. Upon the very first usage of this shortcut, VScode prompts which code formatter to use. You can select black (or autopep8) to enable it.
But, if pressing the shortcut keys bothering you, you can put it on the pre-commit hooks. The below snippet do the trick.
- repo: https://github.com/ambv/black
rev: 21.9b0
hooks:
- id: black
language: python
types: [python]
args: ["--line-length=120"]
Note that this has more settings than the previous ones. Here, in addition to using black, we are overriding its defaults. We used the args option to configure black to set a maximum line length of 120 characters.
Let’s see how git commit hooks work with Black, for an example given in black’s documentation. Create a python file (name doesn’t matter as long as it’s a .py file) with the following content.
from seven_dwwarfs import Grumpy, Happy, Sleepy, Bashful, Sneezy, Dopey, Doc
x = { 'a':37,'b':42,
'c':927}
x = 123456789.123456789E123456789
if very_long_variable_name is not None and \
very_long_variable_name.field > 0 or \
very_long_variable_name.is_debug:
z = 'hello '+'world'
else:
world = 'world'
a = 'hello {}'.format(world)
f = rf'hello {world}'
if (this
and that): y = 'hello ''world'#FIXME: https://github.com/python/black/issues/26
class Foo ( object ):
def f (self ):
return 37*-2
def g(self, x,y=42):
return y
def f ( a: List[ int ]) :
return 37-a[42-u : y**3]
def very_important_function(template: str,*variables,file: os.PathLike,debug:bool=False,):
"""Applies `variables` to the `template` and writes to `file`."""
with open(file, "w") as f:
...
# fmt: off
custom_formatting = [
0, 1, 2,
3, 4, 5,
6, 7, 8,
]
# fmt: on
regular_formatting = [
0, 1, 2,
3, 4, 5,
6, 7, 8,
]
The above file after commit will look like the following. This is more standard compared to the previous one. It’s easy to read, and code reviewers would love to see it this way.
from seven_dwwarfs import Grumpy, Happy, Sleepy, Bashful, Sneezy, Dopey, Doc
x = {"a": 37, "b": 42, "c": 927}
x = 123456789.123456789e123456789
if very_long_variable_name is not None and very_long_variable_name.field > 0 or very_long_variable_name.is_debug:
z = "hello " + "world"
else:
world = "world"
a = "hello {}".format(world)
f = rf"hello {world}"
if this and that:
y = "hello " "world" # FIXME: https://github.com/python/black/issues/26
class Foo(object):
def f(self):
return 37 * -2
def g(self, x, y=42):
return y
def f(a: List[int]):
return 37 - a[42 - u : y ** 3]
def very_important_function(
template: str,
*variables,
file: os.PathLike,
debug: bool = False,
):
"""Applies `variables` to the `template` and writes to `file`."""
with open(file, "w") as f:
...
# fmt: off
custom_formatting = [
0, 1, 2,
3, 4, 5,
6, 7, 8,
]
# fmt: on
regular_formatting = [
0,
1,
2,
3,
4,
5,
6,
7,
8,
]
Sometimes, you want to run pre-commit hooks from your local installed packages. Let’s try to use a locally installed isort package to sort your python imports.
You can install isort using the following command.
pip install isort
Now edit the .pre-commit-config.yaml file and insert the below snippet.
repos:
- repo: local
hooks:
- id: isort
name: Sorting import statements
entry: bash -c 'isort "$@"; git add -u' --
language: python
args: ["--filter-files"]
files: \.py$
To see this in action, create a python file with multiple imports. Here’s a sample.
import os
from my_lib import Object3
from my_lib import Object2
import sys
from third_party import lib15, lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, lib10, lib11, lib12, lib13, lib14
import sys
from __future__ import absolute_import
from third_party import lib3
print("Hey")
print("yo")
After commit, the same file will look like the below.
from __future__ import absolute_import
import os
import sys
from my_lib import Object2, Object3
from third_party import (lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9,
lib10, lib11, lib12, lib13, lib14, lib15)
print("Hey")
print("yo")
Here’s the pre-commit hook template I always use in almost all of my projects. We already discussed two hooks in this list, Black and Isort. Autoflake is another useful hook that removes unused variables, whitespace, and imports.
repos:
- repo: local
hooks:
- id: autoflake
name: Remove unused variables and imports
entry: bash -c 'autoflake "$@"; git add -u' --
language: python
args:
[
"--in-place",
"--remove-all-unused-imports",
"--remove-unused-variables",
"--expand-star-imports",
"--ignore-init-module-imports",
]
files: \.py$
- id: isort
name: Sorting import statements
entry: bash -c 'isort "$@"; git add -u' --
language: python
args: ["--filter-files"]
files: \.py$
- id: black
name: Black Python code formatting
entry: bash -c 'black "$@"; git add -u' --
language: python
types: [python]
args: ["--line-length=120"]
Since this template is using local packages make sure you have them installed. You can run the following command to install them all at once and set up pre-commit to your git repository as well.
pip install isort autoflake black pre-commit
pre-commit install
Git pre-commit is revolutionary in many ways. They are mostly used in CI/CD pipelines to trigger activities. One of the other major use cases is to use them for automatic code formatting.
A well-formatted code is easy to read and digest for a different person as it follows common guidelines shared among the community. Python has a standard called PEP8 and tools like Black, Isort, and Autoflake help developers automate this standardization process.
Yet, it may be a hassle to remember this and using the tool every time manually. Pre-commit hooks quickly put it to its code review checklist and run it automatically before every commit.
In this post, we’ve discussed how to use pre-commit hooks from the remote repositories as well as from locally installed packages.
I hope you’d have enjoyed it.
48