The Power of Pre-Commit for Python Developers: Tips and Best Practices
Tags: python, webdev, programming, git
Published: 2023-05-01 14:44:09

As a software developer, ensuring high-quality and consistent code is vital for seamless operations. Precommit is a powerful tool that can assist in achieving this by automatically checking the code before it's committed. In this article, we'll delve into using precommit with a Python project to enhance code quality and optimize the development process.

Let's have a look what we're going to discuss today:

1. Precommit: How it works?
2. Setup Precommit in Python
3. Write code-checking precommit hooks
4. Customize Precommit
5. Benefits of Precommit in Python
6. Precommit configuration mistakes to avoid
7. Integrate Precommit with CI workflow
8. Best practices for Precommit in Python
9. Conclusion and Next Steps

Then let's start now without any further delay :blush:

## What is Precommit and how does it work?

Precommit is a tool used by developers to ensure code quality by running checks on code before it is committed to the repository. 

This tool relies on a file that contains a list of hooks that execute scripts or commands to check the code. If the code fails the checks, the commit is rejected, and the developer is asked to fix the issues. 

Precommit can be used to enforce consistent code style, syntax errors, and security vulnerabilities. It also supports plugins that can extend its functionality and integrate with other tools like linters, formatters, and static analysis tools.

## Setup Precommit in Python

Running `husky` and `lint-staged` takes much more time than I expected. Sometimes it even took more time than the committing process itself (including checking the files for any errors).

To set up precommit in a Python project, you'll need to follow a few steps.

1. **Install precommit:** First, you need to install precommit as a dev dependency in your project. You can do this by running the following command:

   ```shell
   pip install pre-commit
   ```

2. **Create a precommit configuration file**: Next, create a configuration file for precommit. In this file, you can specify the hooks to run on commit.

   You can do this by adding a `.pre-commit-config.yaml` file to the root of your project. 

3. **Define precommit hooks:** Now, you can define the hooks that precommit will run. 

   For example, you can add a hook to format your code using *black*. You can also add a hook to lint your code using a tool *flake8* .

   Here's an example of how to define a precommit hook for these tasks:

   ```yaml
   repos:
   - repo: https://github.com/psf/black
     rev: 21.7b0
     hooks:
     - id: black
       language_version: python3.8
   
   - repo: https://github.com/PyCQA/flake8
     rev: 3.9.2
     hooks:
     - id: flake8
   ```

   Well discuss about various of these parameters in our coming sections.

4. Run `pre-commit install` to install the Git hooks:

   ```shell
   $ pre-commit install
   ```

5. *Commit your changes to git as usual*. The pre-commit hooks will ***run automatically*** before the commit is created.

6. **Test precommit hooks**: Although ***pre-commit runs automatically*** when you commit your changes to Git. However, if you want you ***can run them manually*** on your codebase. 

   You can do this by running the following command:

   ```shell
   pre-commit run
   ```

   > This will scan only those files which are *staged* in git(through `git add`).

7. **Add and commit changes**: You can now run `git commit` as usual, and *precommit will automatically run* the defined hooks and check your code before committing it.

8. If pre-commit *detects any issues*, it will print a message and prevent the commit from proceeding. You can then fix the issues and try to commit again.

## Write code-checking precommit hooks

We have already seen an example of how to write a pre-commit hook configuration. Now, let's delve deeper into it. Firstly, let's take another look at the sample configuration.

```yaml
repos:
- repo: https://github.com/PyCQA/flake8
  rev: 3.9.2
  hooks:
  -   id: flake8
      name: flake8
      types: [python]
      args:
      -   --max-line-length=88
      -   --ignore=E203,E501,W503
```

This is a pre-commit hook configuration that *installs* and *runs* the `flake8` linter on the specified files in a project. 

The configuration specifies  the repository URL of **flake8** under the `repos` section. It also specifies which version to use. 

The hook has the following properties:

- `id`: The unique identifier of the hook, which is `flake8` in this case.
- `name`: The human-readable name of the hook that would be displayed during run.
- `types`: A list of file types that this hook should be run on. In this case, the hook runs on *python* files.
- `args`: The arguments specifies additional arguments to pass to the command (in this case, `--max-line-length=88` to enforce PEP8 line length limit and `--ignore=E203,E501,W503` to ignore certain Flake8 errors).

Overall, this configuration sets up an *flake8* hook that lints common errors in your python files before they are committed.

> To ensure efficient development processes, it's crucial to consider the performance impact of the checks performed by *precommit* hooks. Long-running checks can significantly impede the team's productivity, so it's vital to optimize these checks.

## Custom hooks to fit your needs

By customizing your precommit configuration, you can ensure that your team's code is held to the highest standards of quality and consistency, while also streamlining your development workflow. 

There are two ways to customize *hooks* as per your needs. We'll discuss both of them.

### Customize existing hook

One way to customize precommit is by *configuring* the built-in hooks. Each precommit hook has a variety of configuration options that you can use to control its behavior. 

For example, you might want to *adjust the severity level* of a particular hook's warnings, or configure a linter to ignore certain directories or files. We already saw this example in our last section.

Adding to the same example, let's ask `flake8` hook to ignore the `venv` directory, you might add the following configuration:

```yaml
repos:
  - repo: https://github.com/pre-commit/mirrors-flake8
    rev: v3.9.2
    hooks:
      - id: flake8
        args: [--exclude=venv]
```

In this example, we're using the `--exclude` option to tell `flake8` to skip any files or directories that match the pattern `venv`.

### Writing Custom hooks from scratch

While precommit comes with a variety of built-in hook. But you can also customize precommit by creating your own hooks. This can be particularly useful if you have specific checks or processes that are unique to your project or team.

You can write hooks in any language (python, c++, javascript, etc.) and use them to run custom scripts or commands on your code. 

To create a custom precommit hook, you first need to *define a new hook in your precommit configuration file*. You'll need to specify the `name` of the hook, the *script or command* to run, and any *arguments* or options that the script requires. 

For example, if you wanted to create a hook that checks for the presence of specific comments in your code, you might create a hook like this:

```yaml
repos:
  - repo: local
    hooks:
      - id: custom-comment-check
        name: Check for Required Comments
        entry: sh
        args: ["-c", "grep -r 'TODO' ."]
```

In this example, we're using the `grep` command to search for the string "TODO" in all files in the current directory and its subdirectories. This will catch any instances of the "TODO" comment that might indicate unfinished code or tasks.

**Notice** that here we used `entry`: The command to execute for this hook, which is `sh` in this case to run the *shell* command.

Once you've defined your custom hook, you can run it along with the other precommit hooks by committing your code. If the hook fails, the commit will be rejected, and you'll be prompted to fix the issue before trying again.

Whether you're creating custom hooks or configuring existing ones, precommit is a powerful tool that can help you take your development process to the next level.

## Benefits of Precommit in Python

Now, let's explore some of the benefits of using precommit in your Python project. By convention, it should have been discussed in the beginning. But I didn't want you to bother with theoretical explainations without showing practical aspect.

1. **Improved Code Quality**: One of the primary benefits is to improve the overall quality of your code. It ensure that code is consistent, readable, and follows best practices. For example, you can use precommit to check for syntax errors, enforce code formatting standards, and detect potential bugs or security vulnerabilities. This ensures that the code you commit is of high quality, reducing the likelihood of bugs or errors that could cause problems down the line.
2. **Reduced Time and Effort**: Precommit automates several aspects of code testing, thereby reducing the time and effort required to manually check code for errors. Instead of running individual checks, precommit runs all checks in a single command, saving developers valuable time that can be used to focus on other tasks.
3. **Easy to Use**: Precommit is easy to set up and use in your Python project. You can install it using pip, and the configuration file can be easily customized to meet the specific needs of your project. Precommit supports a wide range of hooks and linters, making it versatile enough to fit almost any project.
4. **Improved Collaboration**: Precommit helps to improve collaboration among team members. By running precommit checks before pushing changes to the repository, everyone can be confident that the code follows the same standards and conventions. This makes it easier for new team members to get up to speed and maintain consistency throughout the project.

## Precommit configuration mistakes to avoid

While using precommit can significantly improve code quality and development workflow, it's important to configure it correctly to avoid common mistakes that can lead to issues down the line. 

One common mistake is **not specifying the correct version** of a dependency, which can cause compatibility issues with other packages. It's essential to define the correct versions of all packages used in your precommit hooks to ensure compatibility and avoid any unexpected behavior.

Another common mistake is **forgetting to update precommit configuration** when adding new hooks or changing existing ones. It's crucial to keep precommit configuration up to date and test the hooks regularly to catch any issues before they affect the codebase.

## Integrate Precommit with CI workflow

Integrating precommit with your CI workflow can further improve code quality by catching issues before they make it to production. When precommit is integrated with a CI tool like Jenkins or Travis CI, the hooks are automatically run on every code change, ensuring that all code meets the same quality standards. Any failed hooks will prevent the code from being merged into the main branch, reducing the risk of introducing bugs or vulnerabilities.

## Best practices for Precommit in Python

Here are some best practices for using precommit in your Python development workflow:

1. **Use pre-commit.ci**: Pre-commit.ci is a service that runs precommit hooks on your codebase and provides feedback on code quality. It can be integrated with your CI workflow and ensures consistent code quality across all developers.
2. Use a virtual environment: Precommit hooks can require specific versions of libraries and tools. Using a virtual environment ensures that the hooks are run in a consistent environment and reduces the chances of version conflicts.
3. **Configure hooks for the right files:** Some hooks can be expensive and time-consuming. Configuring hooks to run on the right files reduces build times and speeds up the development process.

## Conclusion

In conclusion, Precommit is a powerful tool for improving code quality and consistency in Python projects. 

By setting up custom hooks and integrating it with your CI workflow, you can catch errors and warnings before they are committed, streamlining your development process. With proper configuration and best practices, Precommit can significantly reduce bugs, save time, and enhance the overall quality of your codebase. 

By following the steps outlined in this article, you can easily get started with Precommit and take your Python development to the next level.