Last Updated on November 16, 2023
Table of contents:
- What are Python linters?
- What types of Python linters exist?
- Why should I use Python Linters?
- Why shouldn’t I use Python Linters?
- How to get started with Python linters?
- Black Python Linter: What Is It and How Can It Transform Your Code?
- Pylint Python Linter: Why Is It a Game-Changer for Python Developers?
- Ruff Python Linter guide: How to remove Python code smells?
- MyPy Linter Guide: How Does It Elevate Python Code Quality?
- Bandit Python Linter: Is It the Key to Secure Python Code?
- What are Pre-commit hooks?
- 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