Improving code quality with linting in Python

SnykSec - Oct 18 '22 - - Dev Community

Python is a growing language. As it evolves and expands, so do the number of tools and development strategies available for working with it. One process that’s become increasingly popular is linting — or checking code for potential problems. With linting, errors in our code will be flagged so we can correct unusual programming practices that might result in problems.

Linting is performed while the source code is written and before it’s compiled. In other words, linting is a pre-build check, also called “static code analysis.” Regularly checking our code with linting ensures consistency across the code and the codebase. This minimizes the chances of small errors becoming complex issues after the code is run.

Many developers don’t use linting because they don’t see its added value, as linting won’t prevent bugs. But this perspective undersells the value of linting in improving the quality of the code.

In this hands-on article, we’ll explore how fast and easy it is to perform quick linting checks in Python using Pylint — one of the most popular linting tools. We’ll also see how linting code can help us adhere to the PEP8 code style guide.

Prerequisites

Before you start, ensure you have the following:

  • Python and pip installed on your machine
  • A basic understanding of command-line interfaces (CLIs)
  • An understanding of Python concepts, such as functions and classes

Also, you should note that while the commands shown here are compatible with Linux and macOS-based systems, you should take care when working with Windows.

Linting Python code

Before we dive into how to use a linter in Python, let’s get set up by creating a directory and virtual environment.

Setting up our environment

First, create a directory for the project. For this tutorial, we’ll call it pylint-demo.

$ mkdir pylint-demo

$ cd pylint-demo
Enter fullscreen mode Exit fullscreen mode

Next, create a virtual environment. This will isolate our project dependencies and prevent conflicts with other projects.

$ pip install pipenv

$ pipenv shell
Enter fullscreen mode Exit fullscreen mode

Your prompt should look something like: (pylint-demo) $. This indicates that the virtual environment is active.

With the virtual environment active, install the linter using the following command:

$ pipenv install pylint
Enter fullscreen mode Exit fullscreen mode

We can now run the linter with the pylint command. To ensure that Pylint is successfully installed, run the following command:

$ pylint –help
Enter fullscreen mode Exit fullscreen mode

Getting started with linting

Let’s write a basic Python program and use Pylint on it to see how it works. Create a main.py file and copy in the following code:

def is_number_even(num):
    return "Even" if num % 2 == 0 else "Odd"

num = 5
print(f"The number {num} is {is_number_even(num)}")
Enter fullscreen mode Exit fullscreen mode

In the code above, we’ve added a function to check if the number is even or odd. To use Pylint to check for errors in this code, we use the following command:

$ pylint <<file_name>>
$ pylint main.py
Enter fullscreen mode Exit fullscreen mode

The output of the Pylint is as follows:

************* Module main
main.py:12:0: C0304: Final newline missing (missing-final-newline)
main.py:1:0: C0114: Missing module docstring (missing-module-docstring)
main.py:8:0: C0116: Missing function or method docstring (missing-function-docstring)
main.py:8:19: W0621: Redefining name 'num' from outer scope (line 11) (redefined-outer-name)
main.py:11:0: C0103: Constant name "num" doesn't conform to UPPER_CASE naming style (invalid-name)

------------------------------------------------------------------
Your code has been rated at 0.00/10 (previous run: 0.00/10, +0.00)
Enter fullscreen mode Exit fullscreen mode

We can see several self-explanatory issues in the code, each identified with a character, such as C0304. Pylint applies a letter code to all errors to distinguish the severity and nature of the issue. There are five different categories of errors:

  • C: Convention (for any violation of code convention)
  • R: Refactor (for any issue related to code smell and refactoring)
  • W: Warning (for any programming-level issue that’s not an error)
  • E: Error (for any programming-level issue that is an error)
  • F: Fatal (for any serious issue that stopped Pylint’s execution)

Pylint also gives our code a score out of 10 based on the number of errors present.

In our example, all but one of our error codes are convention errors — with the single error being a warning. To fix these issues, let’s make a few changes in our code and then run Pylint again to see what score our code gets.

""" File contains various function to under Pylint """

def is_number_even(num):
    """Function to check if number is even or odd"""
    return "Even" if num % 2 == 0 else "Odd"

NUM = 5
print(f"The number {NUM} is {is_number_even(NUM)}")
Enter fullscreen mode Exit fullscreen mode

With this code, we added a module and function docstring, a new line at the end, and renamed the variable in the above code. When we rerun Pylint, we get a 10/10 score without any issue.

Running Pylint on a single file

Now that we’re more familiar with how Pylint works, let’s look at another example. Enter the following code:

""" File contains various function to under Pylint """

class animal:
  def __init__ (self, name):
    self.name = name

obj1 = animal("Horse", 21)
print(obj1.name)
Enter fullscreen mode Exit fullscreen mode

In this snippet, we have a simple class named animal and an object of the class named obj1. Now let’s use Pylint on this code.

************* Module main
main.py:4:0: W0311: Bad indentation. Found 2 spaces, expected 4 (bad-indentation)
main.py:5:0: W0311: Bad indentation. Found 4 spaces, expected 8 (bad-indentation)
main.py:3:0: C0115: Missing class docstring (missing-class-docstring)
main.py:3:0: C0103: Class name "animal" doesn't conform to PascalCase naming style (invalid-name)
main.py:3:0: R0903: Too few public methods (0/2) (too-few-public-methods)
main.py:7:7: E1121: Too many positional arguments for constructor call (too-many-function-args)
Enter fullscreen mode Exit fullscreen mode

Notice that while we don’t have code quality issues this time, they’ve been replaced with more substantial errors. With the issues flagged, let’s try to fix them using the code below:

""" File contains various function to under Pylint """

class Animal:
    "Animal Class"
    def __init__ (self, name):
        self.name = name

obj1 = Animal("John")
print(obj1.name)
Enter fullscreen mode Exit fullscreen mode

Then, rerun Pylint.

After changing the class name from animal to Animal, adding a docstring to the class, removing unwanted arguments from function calls, and adding proper indentation, we almost eliminated our code’s errors. There’s still one left, though:

************* Module main
main.py:3:0: R0903: Too few public methods (0/2) (too-few-public-methods)
Enter fullscreen mode Exit fullscreen mode

Let’s see how we can fix this remaining error. Pylint says we don’t have two or more public methods, but there’s a high chance that our code doesn’t have two or more public methods. So how do we fix this?

In an instance like this, we can use Python comments to suppress these issues. The syntax to suppress them is as follows:

# pylint: disable=<<issue_name>>
Enter fullscreen mode Exit fullscreen mode

Here’s how the code will look:

""" File contains various function to under Pylint """

# pylint: disable=too-few-public-methods
class Animal:
    "Animal Class"
    def __init__ (self, name):
        self.name = name

obj1 = Animal("John")
print(obj1.name)
Enter fullscreen mode Exit fullscreen mode

When we check the Pylint output now, we’ll see that the issue is gone.

Running Pylint on a directory

We’ve seen how to run Pylint on a single file, but when working on a project, we won’t have that single file to check for. We’ll need to lint our directory.

To use Pylint on the complete directory, run following command:

$ pylint <<name_of_directory>>
Enter fullscreen mode Exit fullscreen mode

To see how linting a directory works, let’s create two more files and add some code.

$ mkdir src; cd src
$ touch helpers.py config.py __init__.py
Enter fullscreen mode Exit fullscreen mode

Move the main.py file to the src directory and paste the following code into the respective files:

<<main.py>>
""" File contains various function to under Pylint """

from helpers import connect_db
from config import DB_USER, DB_PASS

is_connected = connect_db(DB_USER, DB_PASS)

if is_connected:
    print("Connected to DB")
else:
    print("Failed to connect to DB")

<<helpers.py>>
def connect_db(user, password):
    """Dummy function to connect to DB"""
    if user is None or password is None:
        return False
    return True

<<config.py>>
DB_USER = "root"
DB_PASS = "toor"
Enter fullscreen mode Exit fullscreen mode

We have three files in the src directory: main.py, helpers.py, and config.py. In main.py, we have a dummy function that prints whether we’re connected to DB or not. helpers.py contains a dummy helper function to connect to DB, and the config.py file contains the DB username and password.

Now, let’s run Pylint on the whole directory using the following command from the root directory:

$ pylint src
Enter fullscreen mode Exit fullscreen mode

The output of the command will be as follows:

************* Module src.config
src/config.py:2:0: C0304: Final newline missing (missing-final-newline)
src/config.py:1:0: C0114: Missing module docstring (missing-module-docstring)
************* Module src.main
src/main.py:11:0: C0304: Final newline missing (missing-final-newline)
src/main.py:3:0: E0401: Unable to import 'helpers' (import-error)
src/main.py:4:0: E0401: Unable to import 'config' (import-error)
************* Module src.helpers
src/helpers.py:6:0: C0304: Final newline missing (missing-final-newline)
src/helpers.py:1:0: C0114: Missing module docstring (missing-module-docstring)
Enter fullscreen mode Exit fullscreen mode

As we can see, Pylint shows us output for different files separated by *** and the module name. To fix the issues, we need to make the following changes:

  • Add a new line at the end of each file.
  • Add a docstring to each file and function.
  • Modify the import statement from helpers import connect_db to .helpers import connect_db.

Once we fix these issues, we’ll see another issue — we need to capitalize the is_connected variable. We can either change the variable name, or suppress the warning to handle this error.

Suppressing warnings

There’s a high chance you’ll need to customize or suppress multiple warnings while linting your Python code. Adding a comment each time wouldn’t make sense. Instead of working through warning suppression instances one by one, you can create an .rc file to customize Pylint behavior and suppress the warning for the whole project directly from the .rc file.

You can create one using the following command:

$ pylint --generate-rcfile > pylint.rc
Enter fullscreen mode Exit fullscreen mode

Better, more secure code with linting

Linting in Python checks source code as it’s written and flags errors along the way — before we run the code. You can also embed Pylint into editors to view the linting in real time.

Although linting doesn’t automatically fix bugs, using it consistently helps ensure that our code quality remains high. So, while some developers view linting as a waste of time, it is extremely effective at catching small problems before they snowball into larger ones.

Throughout this article, we’ve explored how linting and implementing Pylint’s recommendations improved our sample code. Moreover, this process inherently helps us adhere to the PEP8 style guide. Now that you can implement linting in your projects, you can explore the many available linting tools and determine which best complements — and enhances — your approach to Python development.

Python security made simple — and free

Create a free Snyk account today to secure your Python code.

Sign up for free

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .