DEV Community

Cover image for Git Pre-commit Hooks for Automatic Python Code Formatting
Thuwarakesh Murallie
Thuwarakesh Murallie

Posted on • Updated on • Originally published at the-analytics.club

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.)

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 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"]
Enter fullscreen mode Exit fullscreen mode

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,
]
Enter fullscreen mode Exit fullscreen mode

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,
]
Enter fullscreen mode Exit fullscreen mode

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 package to sort your python imports.

You can install isort using the following command.

pip install isort
Enter fullscreen mode Exit fullscreen mode

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$
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 because I post more like this frequently.

Thanks for reading, friend! Say Hi to me on LinkedIn, Twitter, and Medium.

Top comments (1)

Collapse
 
jesseinit profile image
Jesse Egbosionu

Nice post.
Funny how I just added linting and formatted a 2 year old codebase at work last week. I used most of these, also added a CI step to check for linting errors.