Git Pre-commit Hooks for Automatic Python Code Formatting | 1
| |

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 like 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 a 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.

 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 install Python (as it’s the language used for git hooks.)

 

Related: 7 Ways to Make Your Python Project Structure More Elegant

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.

pip install pre-commit
Bash

The .pre-commit-config.yaml file holds all the configurations your project requires. This is where you tell the pre-commit what actions it needs to perform before every commit and override its defaults if needed.

The following command will generate an example configuration file.

pre-commit sample-config > .pre-commit-config.yaml
Bash

Here is an example configuration that sorts your requirements.txt file every time before you commit 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
YAML

We’ve installed the pre-commit package and configured how it should work. Now, we can enable pre-commit hooks to this repository using the following command.

Awesome! Let’s add the following requirements.txt file to your project and make a git commit. See pre-commit hooks in action.

# requirement.txt before git commit.
urllib3==1.26.7
openpyxl==3.0.9
pandas==1.3.3
YAML

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 one below.

# requirement.txt after commit.
openpyxl==3.0.9
pandas==1.3.3
urllib3==1.26.7
YAML

Related: How to Run Python Tests on Every Commit Using GitHub Actions

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

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 to clean your code on the go. For example, VSCode on Linux uses Ctrl + Shift + I. Upon first using this shortcut, VScode prompts which code formatter to use. You can select black (or autopep8) to enable it.

But, if pressing the shortcut keys bothers you, you can put it on the pre-commit hooks. The below snippet does the trick.

-   repo: https://github.com/ambv/black
    rev: 21.9b0
    hooks:
    - id: black
      language: python
      types: [python]
      args: ["--line-length=120"]
YAML

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 hooks work with Black, for example, given in Black’s documentation. Create a Python file (the 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,
]
Python

The above file, after committing, 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 = r"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,
]
Python

Configure pre-commit hooks to look for local repositories

Sometimes, you want to run pre-commit hooks from your locally 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
Bash

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$
YAML

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")
Python

After committing, the same file will look like the one 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")
Python

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 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"]
YAML

Since this template uses 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.

pip install isort autoflake black pre-commit
pre-commit install
Bash

Final thoughts

Git pre-commit is revolutionary in many ways. They are primarily 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 standard 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 use the tool every time manually. Pre-commit hooks quickly put it to its code review checklist and run it automatically before every commit.

This post discussed using pre-commit hooks from remote repositories and locally installed packages.

I hope you’d have enjoyed it.


Thanks for the read, friend. It seems you and I have lots of common interests. Say Hi to me on LinkedIn, Twitter, and Medium.

Not a Medium member yet? Please use this link to become a member because I earn a commission for referring at no extra cost for you.

Similar Posts