Mastering Python Linters: A Guide for Developers

9 min read

Get 10-day Free Algo Trading Course

Last Updated on November 16, 2023

Table of contents:

  1. What are Python linters?
  2. What types of Python linters exist?
  3. Why should I use Python Linters?
  4. Why shouldn’t I use Python Linters?
  5. How to get started with Python linters?
  6. Black Python Linter: What Is It and How Can It Transform Your Code?
    1. How to install Black
    2. How to use Black
    3. How to configure Black
  7. Pylint Python Linter: Why Is It a Game-Changer for Python Developers?
    1. How to install Pylint?
    2. How to use Pylint?
    3. How to configure Pylint?
  8. Ruff Python Linter guide: How to remove Python code smells?
    1. How to install Ruff?
    2. How to use Ruff?
    3. How to configure Ruff?
  9. MyPy Linter Guide: How Does It Elevate Python Code Quality?
  10. Bandit Python Linter: Is It the Key to Secure Python Code?
    1. How to install Bandit?
    2. How to use Bandit?
    3. How to configure Bandit?
  11. What are Pre-commit hooks?
  12. How to use Python Linters with Pre-commit hooks?

What are Python linters?

Python linters are tools for developers that do automated code analysis. They play a crucial role in improving code quality by scanning Python code to identify syntax errors, stylistic issues, and complex constructs that deviate from best coding practices.

What types of Python linters exist?

Types of Python linters can be categorized into two groups which are Code formatting and style and Error Detection.

The first group checks if the code adheres to the stylistic guidelines, such as PEP 8, which is the style guide for Python code. This includes checking indentation, line spacing, and the use of variables and function names.

The second group uncovers potential errors before the code is executed. They pinpoint issues like undeclared variables, possible syntax errors, and other inconsistencies that could lead to bugs.

Sometimes, some linters fall into both groups and we will cover them in this article.

Why should I use Python Linters?

  • Python linters make your code cleaner.
  • Make your code easier to maintain and read.
  • Spot bugs and errors in your code in time.
  • Allow for a better collaboration experience.
  • It makes you more productive.
  • It teaches you to write better code.

Why shouldn’t I use Python Linters?

  • Python linters can slow down prototyping and fast iterating.
  • Over-reliance leads to a false sense of security.
  • Can bring much overhead on small and simple projects.
  • Can have a higher learning curve for Python beginners.

How to get started with Python linters?

To get started with Python linters, all you need to do is to install them via pip and configure them if needed. Most linters prefer pure Python files (e.g. ruff) while others can also style your notebooks (e.g. black).

The linters that we will take a look at are the linters I personally use for all my projects which are the following:

  • Black
  • Pylint
  • Ruff
  • MyPy
  • Bandit
  • PyDocstyle

We will look into each one of them and I’ll show you how to work with them. Then, we’ll combine them together into a pre-commit hook that will check all of our files and stop the commit if we have linting errors.

Black Python Linter: What Is It and How Can It Transform Your Code?

Black is a Python code formatter known for its uncompromising approach to code styling. It prioritizes consistency and determinism in code formatting for a uniform style. It aims for simplicity and minimization of diff sizes.

Link to repository: https://github.com/psf/black

How to install Black?

To install Black, all you need to do is write:

pip install black

If you want black to work with your notebook files, you can additionally write:

pip install "black[jupyter]"

How to use Black

Using Black is as simple as running a single command. To format a single Python file:

black your_script.py

To format a directory, you can do:

black my_dir

To format all files at your current location do:

black .

For example, here is a small code block before Black is applied:

def my_function(arg1,arg2):
    print( "arg1:",arg1,"arg2:",arg2)

Here is the after:

def my_function(arg1, arg2):
    print("arg1:", arg1, "arg2:", arg2)

How to configure Black?

Black aims to be an opinionated formatter, so configuration options are minimal. However, you can configure line length (default is 88 characters) and exclude specific files. For example, to set a line length of 100 characters:

black your_script.py -l 100

To exclude a directory or a file, use the --exclude parameter. Here’s how to exclude a directory named migrations:

black your_directory --exclude='/migrations/'

Configuration can also be specified in a pyproject.toml file, which Black will automatically detect and use. For example:

[tool.black]
line-length = 100
exclude = '''
/(
    migrations
)/
'''

The code block above combines the two configuration options we ran manually. This way, you can run black without the need to pass extra arguments.

Pylint Python Linter: Why Is It a Game-Changer for Python Developers?

Pylint is a versatile Python linter for static code analysis. It checks Python code against a wide range of programming standards, highlights errors, and enforces a more explicit coding standard.

It offers detailed reports on code quality, potentially problematic areas, code duplication, styling issues, and more. It is quite customizable and supports plugins.

Link to repository: https://github.com/pylint-dev/pylint

How to install Pylint?

Installing Pylint is straightforward and can be done using pip:

pip install pylint

How to use Pylint?

To use Pylint, simply run it against a Python file or a module. For example, to lint a single file:

pylint your_script.py

For linting an entire Python package:

pylint your_package/

Pylint will analyze the code and output a report detailing various issues, categorized by their nature (e.g., errors, warnings, refactor suggestions).

Here is an example code block before abiding by Pylint:

class my_class:
    def func1(self):
        pass

    def anotherFunction(self, arg1):
        self.myvar = arg1
        print(arg1)

obj = my_class()
obj.func1()
obj.anotherFunction(10)

Here is the code block after cleaning out Pylint errors:

class MyClass:
    """Example class demonstrating Pylint improvements."""

    def __init__(self):
        """Initialize the class."""
        self.my_var = None

    def function_one(self):
        """Example method that does nothing."""
        # Previously had 'pass', removed as it's unnecessary here.

    def another_function(self, arg1):
        """Print the provided argument.

        Args:
            arg1 (int): The argument to be printed.
        """
        self.my_var = arg1
        print(arg1)

obj = MyClass()
obj.function_one()
obj.another_function(10)

Sometimes, Pylint might be wrong in its interpretation. In that case, you can ignore specific Pylint errors either in your entire file, specific line, specific function/class, or more.

For instance, if you want to ignore a particular warning, say line-too-long (C0301), on a specific line, you can do the following:

some_really_long_line = '...'  # pylint: disable=line-too-long

You can also just write the code of the error but that makes your ignored error less explicit. To disable a warning for an entire file, add a comment at the top of the file:

# pylint: disable=line-too-long

How to configure Pylint?

Pylint is highly customizable. You can configure it by creating a .pylintrc file in your project’s root directory. Here’s a simple example of a .pylintrc file:

[MASTER]
disable=
    C0111, # missing-docstring
    C0103  # invalid-name

[MESSAGES CONTROL]
max-line-length=100

Ruff Python Linter guide: How to remove Python code smells?

Ruff is a robust and efficient Python linter written in Rust that aims for speed and simplicity. It performs rapid code checks and can fix common code issues, syntax errors, and style issues by itself. It is configurable and lightweight which makes it a great linter for CI/CD pipelines.

Link to repository: https://github.com/astral-sh/ruff

How to install Ruff?

To install Ruff, use the following pip command:

pip install ruff

How to use Ruff?

Using Ruff is straightforward. To analyze a single Python file:

ruff check your_script.py

And to lint an entire project:

ruff --fix .

Here is an example code block before Ruff:

def calculate_area( length, width ):
    area=length*width
    print("Area:",area)
    return(area)

calculate_area(10,20)

Here is the after:

def calculate_area(length, width):
    area = length * width
    print("Area:", area)
    return area

calculate_area(10, 20)

These corrections align the code with Python’s PEP 8 style guide, improving readability and maintainability. In a real-world scenario, a linter like Ruff would also flag other potential issues like variable naming conventions, line lengths, and more complex stylistic concerns.

Sometimes, some Ruff errors might not make perfect sense for your code implementation or you might be too lazy to fix it. Sometimes, it might reduce some efficiency so you can ignore errors like this:

def legacy_data_formatter(data):
    # Legacy system requires old-style %-formatting, not .format() or f-strings
    # ruff: ignore=modern-string-formatting
    formatted_data = "Name: %s, Age: %d" % (data['name'], data['age'])
    return formatted_data

data = {'name': 'Alice', 'age': 30}
formatted_data = legacy_data_formatter(data)
print(formatted_data)

How to configure Ruff?

Ruff can be configured to suit specific project requirements. Configuration typically involves creating a .ruffrc file in the root directory of your project. Here’s an example of what the configuration file might look like:

[ruff]
ignore = E203, W503
max-line-length = 120
select = C,E,F,W,B,B950
exclude = .venv,.git,__pycache__,old,build,dist

In this .ruffrc file, we’re setting specific rules to ignore, defining the maximum line length, selecting the error codes to check for, and excluding directories from linting.

MyPy Linter Guide: How Does It Elevate Python Code Quality?

MyPy is a great static type checker for Python that helps catch type inconsistencies in code. By enforcing type hints, MyPy ensures that functions and variables are used correctly based on their data types which reduces runtime errors.

Link to repository: https://github.com/python/mypy

How to install MyPy?

Installing MyPy is straightforward with pip:

pip install mypy

How to use MyPy?

To use MyPy, simply run it against a Python file or directory. For example, to type check a single file:

mypy script.py

Or to check an entire project:

mypy your_project_directory/

MyPy analyzes the annotated types in your Python code, reports discrepancies, and suggests corrections where type mismatches are detected. You might notice that it is quite slower to run when compared to the previous linters. This is because it is written in Python. 😀

Here is an example code block before MyPy:

def square_numbers(numbers):
    return {number: number**2 for number in numbers}

result = square_numbers([1, 2, 3, 4])
print(result)

Here is an example code block after implementing typing and abiding by MyPy suggestions:

from typing import List, Dict

def square_numbers(numbers: List[int]) -> Dict[int, int]:
    return {number: number**2 for number in numbers}

result = square_numbers([1, 2, 3, 4])
print(result)

Sometimes, MyPy might be wrong in its interpretation as Python isn’t a static language and sometimes it doesn’t make sense to force it to be one. In that case, you can ignore specific MyPy errors either in your entire file, specific line, specific function/class, or more.

Suppose you have a function that interacts with a third-party library where the return type of a function is not clearly defined (e.g., it could return different types based on certain conditions). However, you know from the docs that under certain conditions, the return type will be an integer.

from typing import Any
from some_third_party_library import get_dynamic_value

def calculate_value() -> int:
    value: Any = get_dynamic_value()
    # Based on certain conditions, you know 'value' will be an integer
    return value  # MyPy will flag this as an error

We can ignore the error like this:

def calculate_value() -> int:
    value: Any = get_dynamic_value()
    return value  # type: ignore

How to configure MyPy?

MyPy can be configured via a mypy.ini file in your project root. This file allows you to set various options to tailor MyPy’s behavior to your project’s needs. Here’s an example configuration:

[mypy]
ignore_missing_imports = True
warn_redundant_casts = True
warn_unused_ignores = True
strict_optional = True

In this example configuration:

  • ignore_missing_imports avoids warnings for missing type stubs of external libraries.
  • warn_redundant_casts alerts when a type cast is unnecessary.
  • warn_unused_ignores flags any unnecessary ‘type: ignore’ comments in the code.
  • strict_optional enforces stricter checking of optional types.

These settings help you customize MyPy’s strictness and reporting to align with your project’s requirements and coding standards.

Bandit Python Linter: Is It the Key to Secure Python Code?

Bandit is a tool designed to find common security issues in Python code. Unlike other linters that focus on code style and formatting, Bandit scans for potential security vulnerabilities. It examines each file, builds an abstract syntax tree (AST), and runs appropriate plugins to test for various security risks.

Link to repository: https://github.com/PyCQA/bandit

How to install Bandit?

To install Bandit, you can use pip like so:

pip install bandit

How to use Bandit?

Once installed, you can run Bandit on your Python files or projects to check for security issues. Here’s how to run it on a single file:

bandit -r your_script.py

For scanning an entire project directory, use:

bandit -r /path/to/your/project

Bandit will recursively scan all the Python files in the specified directory and output any security warnings.

How to configure Bandit?

Bandit allows customization through a configuration file where you can specify which tests to run, which to skip, and other settings. The configuration file is typically named .bandit.yml or bandit.yaml. Here’s an example of a basic Bandit configuration file:

tests:
  - assert_used
  - exec_used
  - hardcoded_bind_all_interfaces

skips:
  - B101:assert_used

What are Pre-commit hooks?

Pre-commit hooks are an essential tool for maintaining code quality and consistency. They are automated scripts that run checks before a commit is made to a repository, ensuring that all changes meet the required standards.

This will result in a very powerful tool that will help you make your code of high quality.

How to use Python Linters with Pre-commit hooks?

To use Python linters with pre-commit hooks, you first need to install the pre-commit framework. You can do this using pip:

pip install pre-commit

After installation, create a .pre-commit-config.yaml file in the root directory of your project. This file will define which hooks (linters in this case) you want to run.

In your .pre-commit-config.yaml, you can specify various linters (like Black, Pylint, MyPy, Bandit, and others) as individual hooks. Here’s an example configuration:

repos:
  - repo: https://github.com/psf/black
    rev: stable
    hooks:
      - id: black

  - repo: https://github.com/PyCQA/pylint
    rev: pylint-2.9.6
    hooks:
      - id: pylint

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v0.910
    hooks:
      - id: mypy

  - repo: https://github.com/PyCQA/bandit
    rev: 1.7.0
    hooks:
      - id: bandit

Each - repo: section specifies a linter’s repository, the version (rev:), and the id of the hook.

After setting up the configuration file, you need to install the hooks. Run the following command in your project directory:

pre-commit install

With the hooks installed, they will automatically run on every git commit. If a hook finds issues, the commit will be blocked until those issues are resolved, ensuring that your code adheres to the standards set by the linters.

You can also manually run all the hooks against all the files in your repository with:

pre-commit run --all-files
Igor Radovanovic