First things first

Hello everyone, I’m Thanh Tung, a developer with N years of experience.

When working in teams, each person has their own coding style, which makes code reading and maintenance difficult, leading to increased development time and reduced project efficiency.

This motivated me to write this article to share my experience using tools to improve code quality, specifically for Python.

Here are some reasons to standardize code:

  • Code becomes more readable and maintainable
  • Reduces bugs and potential errors
  • Improves code review efficiency
  • Ensures everyone in the team follows the same standards
  • And so on and so forth

If you’re looking for ways to improve your Python code quality, this article is for you.

Let’s get started!

My example repo

Main tools I use

1. Black - The Uncompromising Code Formatter

Black is an automatic code formatter for Python. What’s special about Black is that it’s “uncompromising” - meaning you don’t need much configuration, Black will format code according to a specific standard.

Installing Black:

pip install black

Using Black:

Format a single file

black your_file.py

Format all files in a directory

black .

Example

Before using Black:

def foo(

):
    print ("Hello world!")
    print   ("Hello world!")
    print   (
        "Hello world!"
        )

After using Black:

def foo():
    print("Hello world!")
    print("Hello world!")
    print("Hello world!")

Looks better now, doesn’t it?

2. Flake8 - Style Guide Enforcement

Flake8 is a popular linting tool that combines features from PyFlakes, pycodestyle, and Ned Batchelder’s McCabe script.

Installing Flake8:

pip install flake8

Using Flake8:

flake8 your_file.py

Example

Invalid code:

def my_function():
    # This is a really long comment that exceeds the 79-character limit and causes a line length error in Flake8.
    return 42

Flake8 will report:

examples/use_flake8.py:3:80: E501 line too long (113 > 79 characters)

Valid code:

def my_function():
    # This is a really long comment that exceeds the 79-character limit 
    # and causes a line length error in Flake8.
    return 42

3. Pylint - A Tool for Style Checks

Pylint is a Python code analysis and evaluation tool. It checks code against various standards, from line length, nesting depth, to the usage of variables, functions, classes, and more.

Installing Pylint:

pip install pylint

Using Pylint:

pylint your_file.py

Example

Invalid code:

def my_function():
    return 42

Pylint will report:

# ************* Module try_pylint
# your_file.py:2:0: C0304: Final newline missing (missing-final-newline)
# your_file.py:1:0: C0114: Missing module docstring (missing-module-docstring)

# -----------------------------------
# Your code has been rated at 0.00/10

Valid code:

"""This is a docstring for the module."""
def my_function():
    """This is a docstring for the function."""
    return 42

You might wonder what’s the difference between Flake8 and Pylint since they’re both linters?

The answer is yes, there are differences:

  • Flake8 is a simpler linter, checking only basic issues like line length, formatting, etc.
  • Pylint is more complex, checking more issues including line length, formatting, etc.

Specifically: see more

ProblemPylintFlake8
Unnecessary semicolon
Missing newlines
Missing whitespaces
Missing docstring
Unused arguments
Incorrect naming style
Selecting element from the list with incorrect index
Dividing by zero expression
Missing return statement
Calling method that doesn’t exist
Unnecessary pass statement

4. Commitlint - Linting Git Commit Messages

Commitlint is a commit message linting tool that helps ensure commit messages follow a specific standard.

This reminds me of my colleagues who often write very messy commit messages :))
Bad Commit Message

Or like this:

Bad Commit Message

Of course, writing like this makes it incomprehensible to anyone, including the author (after a few days). To solve this issue, I started using Commitlint.

Commitlint rules (default):

  • Commit message header should be between 10 and 50 characters

    • => No more test, fix bug or update ui commits ^^
  • Commit message body should be between 0 and 72 characters

  • Commit message footer should be between 0 and 72 characters

  • Commit message structure following Conventional Commits:

    • Must start with a type (feat, fix, docs, style, refactor, test, chore)
    • Type is followed by : and description
    • Example: feat: add new login feature
    • Types can be:
      • build: Changes that affect build system or dependencies
      • ci: Changes to CI configuration files and scripts
      • chore: Minor changes (e.g., .gitignore, .pre-commit-config.yaml)
      • docs: Documentation only changes
      • feat: Adding/removing features
      • fix: Bug fixes
      • perf: Performance improvements
      • refactor: Code changes that neither fix bugs nor add features
      • revert: Reverting previous commits
      • style: Changes that don’t affect code meaning
      • test: Adding/modifying tests

See more: Conventional Commits

So how do we use Commitlint? Let’s move to the next section.

Pre-commit - Git Hooks Made Easy

Pre-commit helps automate code checking before committing, ensuring code always meets standards before being pushed to the repository.

Install pre-commit (one time):

pip install pre-commit

Configure pre-commit hooks in .pre-commit-config.yaml:

  • Configure Black
repos:
...
-   repo: https://github.com/psf/black
    rev: 25.1.0
    hooks:
    -   id: black
        language_version: python3.10
  • Configure Flake8
repos:
...
-   repo: https://github.com/PyCQA/flake8
    rev: 7.1.2
    hooks:
    -   id: flake8
        language_version: python3.10
  • Configure Pylint
repos:
...
-   repo: https://github.com/pylint-dev/pylint
    rev: v3.3.4
    hooks:
    -   id: pylint
        language_version: python3.10
  • Configure Commitlint
repos:
...
-   repo: https://github.com/opensource-nepal/commitlint
    rev: v1.3.0
    hooks:
    - id: commitlint

Install commit hooks:

pre-commit install
pre-commit install --hook-type commit-msg

After installation, pre-commit will check your code every time you commit and report errors if standards aren’t met.

IDE Configuration

Besides using pre-commit to check code, you can configure your IDE to check code before committing.

VSCode

  • Install extension: Black Formatter
  • Configure:
{
  "python.formatting.provider": "black"
}

flake8

  • Install extension: flake8
  • Configure:
{
  "python.linting.flake8Enabled": true
}

pylint

  • Install extension: pylint
  • Configure:
{
  "python.linting.pylintEnabled": true
}

CI/CD Integration

Integrating checking tools into CI/CD will help you verify code one more time before merging into the main branch. This is especially useful for cases like:

  • git commit –no-verify
  • adding files on gitlab site

Gitlab CI

Here, I combine format and lint into one job to optimize build time.

The lint stage will run the tools in sequence: black, flake8, pylint, and commitlint, and only runs when the pipeline is a merge request and the target branch is master or release-*

stages:
  - lint
  - test
  - build
  - deploy

lint:
  image: python:3.10-slim
  stage: lint
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event' && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^(master|release\-.*)$/ # merge request
  environment: $ENVIRONMENT
  script:
    - pip install -r requirements-dev.txt
    - echo "Running linting with black..."
    - black --check .
    - echo "Running linting with flake8..."
    - flake8 .
    - echo "Running linting with pylint..."
    - pylint .
    - echo "Running linting with commitlint..."
    - commitlint "$CI_COMMIT_MESSAGE"

  cache:
    paths:
      - .cache/pip
  allow_failure: false

Conclusion

Setting up code quality checking tools might take some initial time, but the benefits they bring are well worth it. Not only does it make your code cleaner, but it also helps the entire team work more effectively.

I hope this article helps you set up a professional Python development environment. See you in the next articles! 😊

My example repo

References