Git Pre-commit Hooks for Automatic Python Code Formatting
Tags: python, webdev, programming, devops
Published: 2022-01-04 07:48:00

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](https://pre-commit.com/) 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.)

Related: [___7 Ways to Make Your Python Project Structure More Elegant___](/python-project-structure-best-practices)

## Installing pre-commit 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.

```shell
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.


```shell
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.

```yaml
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
```

---

## Black with pre-commit hooks to automatically format your Python code.

[Black](https://github.com/psf/black) is a popular package that helps Python developers maintain a clean codebase.

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.

```yaml
-   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.


```python
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.


```python
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,
]
```

## Configure pre-commit hooks to look for local repositories

Sometimes, you want to run pre-commit hooks from your local installed packages. Let’s try to use a locally installed [isort](https://github.com/PyCQA/isort) package to sort your python imports.

You can install isort using the following command.

```shell
pip install isort
```

Now edit the .pre-commit-config.yaml file and insert the below snippet.

```yaml
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.

```python 
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.

```python 
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")
```

### Use Black, Isort, and Autoflake pre-commit hooks for a cleaner python codebase.

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](https://github.com/myint/autoflake) is another useful hook that removes unused variables, whitespace, and imports.

```yaml
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.

```shell
pip install isort autoflake black pre-commit
pre-commit install
```

## Final thoughts

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.

---
Did you like what you read? [**_Consider subscribing to my email newsletter_**](https://thuwarakesh.medium.com/subscribe)  because I post more like this frequently. 

Thanks for reading, friend! Say Hi to me on [LinkedIn](https://www.linkedin.com/in/thuwarakesh/), [Twitter](https://twitter.com/Thuwarakesh), and [Medium](https://thuwarakesh.medium.com/).