Improving code quality with linting in Python
October 12, 2022
0 mins readPython 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
.
1$ mkdir pylint-demo
2
3$ cd pylint-demo
Next, create a virtual environment. This will isolate our project dependencies and prevent conflicts with other projects.
1$ pip install pipenv
2
3$ pipenv shell
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:
1$ pipenv install pylint
We can now run the linter with the pylint
command. To ensure that Pylint is successfully installed, run the following command:
1$ pylint –help
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:
1def is_number_even(num):
2 return "Even" if num % 2 == 0 else "Odd"
3
4num = 5
5print(f"The number {num} is {is_number_even(num)}")
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:
1$ pylint <<file_name>>
2$ pylint main.py
The output of the Pylint is as follows:
1************* Module main
2main.py:12:0: C0304: Final newline missing (missing-final-newline)
3main.py:1:0: C0114: Missing module docstring (missing-module-docstring)
4main.py:8:0: C0116: Missing function or method docstring (missing-function-docstring)
5main.py:8:19: W0621: Redefining name 'num' from outer scope (line 11) (redefined-outer-name)
6main.py:11:0: C0103: Constant name "num" doesn't conform to UPPER_CASE naming style (invalid-name)
7
8------------------------------------------------------------------
9Your code has been rated at 0.00/10 (previous run: 0.00/10, +0.00)
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.
1""" File contains various function to under Pylint """
2
3def is_number_even(num):
4 """Function to check if number is even or odd"""
5 return "Even" if num % 2 == 0 else "Odd"
6
7NUM = 5
8print(f"The number {NUM} is {is_number_even(NUM)}")
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:
1""" File contains various function to under Pylint """
2
3class animal:
4 def __init__(self, name):
5 self.name = name
6
7obj1 = animal("Horse", 21)
8print(obj1.name)
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.
1************* Module main
2main.py:4:0: W0311: Bad indentation. Found 2 spaces, expected 4 (bad-indentation)
3main.py:5:0: W0311: Bad indentation. Found 4 spaces, expected 8 (bad-indentation)
4main.py:3:0: C0115: Missing class docstring (missing-class-docstring)
5main.py:3:0: C0103: Class name "animal" doesn't conform to PascalCase naming style (invalid-name)
6main.py:3:0: R0903: Too few public methods (0/2) (too-few-public-methods)
7main.py:7:7: E1121: Too many positional arguments for constructor call (too-many-function-args)
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:
1""" File contains various function to under Pylint """
2
3class Animal:
4 "Animal Class"
5 def __init__(self, name):
6 self.name = name
7
8obj1 = Animal("John")
9print(obj1.name)
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:
1************* Module main
2main.py:3:0: R0903: Too few public methods (0/2) (too-few-public-methods)
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:
1# pylint: disable=<<issue_name>>
Here’s how the code will look:
1""" File contains various function to under Pylint """
2
3# pylint: disable=too-few-public-methods
4class Animal:
5 "Animal Class"
6 def __init__(self, name):
7 self.name = name
8
9obj1 = Animal("John")
10print(obj1.name)
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:
1$ pylint <<name_of_directory>>
To see how linting a directory works, let’s create two more files and add some code.
1$ mkdir src; cd src
2$ touch helpers.py config.py __init__.py
Move the main.py
file to the src
directory and paste the following code into the respective files:
1<<main.py>>
2""" File contains various function to under Pylint """
3
4from helpers import connect_db
5from config import DB_USER, DB_PASS
6
7is_connected = connect_db(DB_USER, DB_PASS)
8
9if is_connected:
10 print("Connected to DB")
11else:
12 print("Failed to connect to DB")
13
14<<helpers.py>>
15def connect_db(user, password):
16 """Dummy function to connect to DB"""
17 if user is None or password is None:
18 return False
19 return True
20
21<<config.py>>
22DB_USER = "root"
23DB_PASS = "toor"
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:
1$ pylint src
The output of the command will be as follows:
1************* Module src.config
2src/config.py:2:0: C0304: Final newline missing (missing-final-newline)
3src/config.py:1:0: C0114: Missing module docstring (missing-module-docstring)
4************* Module src.main
5src/main.py:11:0: C0304: Final newline missing (missing-final-newline)
6src/main.py:3:0: E0401: Unable to import 'helpers' (import-error)
7src/main.py:4:0: E0401: Unable to import 'config' (import-error)
8************* Module src.helpers
9src/helpers.py:6:0: C0304: Final newline missing (missing-final-newline)
10src/helpers.py:1:0: C0114: Missing module docstring (missing-module-docstring)
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 fromhelpers 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:
1$ pylint --generate-rcfile > pylint.rc
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.
Get started in capture the flag
Learn how to solve capture the flag challenges by watching our virtual 101 workshop on demand.