Python Linter Showdown

Which linter shall reign supreme?

Four linters enter! One linter leaves?

Python Linter Showdown! It’s time for a spirited comparison of four popular Python linters: black, flake8, pylint, and ruff. Each tool promises to tidy up your code, catch pesky bugs, and keep your scripts neat. Our contestants today will face off on three files containing some not-so-great Python. Get ready to witness our code-based house of horror!

A spirited competition between Python linters

First, let’s introduce the challengers:

black

black is all about automated code formatting. Rather than fussing with how many spaces go here or where a line break should go, black steps in and enforces a standard layout. It fixes indentation and spacing but does not do much about deeper coding issues like unused variables or suspicious naming. If you crave a no-nonsense formatter that gets your code looking consistent, black is the champion. However, it can be a bit strict about style and will enforce its rules whether you like them or not.

flake8

flake8 is a combination of three pillars: PyFlakes, pycodestyle, and McCabe. You get syntax and style checks aligned with PEP 8, plus some cyclomatic complexity warnings to flag overly complex blocks of code. One plus is that flake8 is fast and straightforward for novices. On the down side, it does not automatically fix your code. It just points out the trouble spots for you. Think of flake8 as the responsible friend who quietly notes your mistakes and leaves the actual repair work to you.

pylint

pylint is the thorough investigator. This tool combs through your code and pronounces its judgments on style, logic, naming, docstring coverage, import usage, and more. You can get a numeric score rating how well you follow best practices. It is the old pro who sometimes seems too picky, but you will be grateful when it flags an overlooked bug. The con is that pylint can be noisy if you don’t configure it to your project. You might feel like it is complaining about every line, but that can be worthwhile if you want meticulous coverage.

ruff

ruff is the new speed demon on the scene. It can run many linting checks with lightning efficiency. Because it is written in Rust, ruff can reveal style and logical issues nearly instantly, even in large projects. It’s also easy to configure. The drawback is that it is still catching up with the full feature set of older tools. But if you want a fast solution that can check a range of issues, ruff is definitely worth a look.

Next, meet the obstacle course:

We challenged a few LLMs to create a linter torture chamber, and came up with three devious files for each linter to chew on. These files are riddled with questionable formatting, odd indentation, messy imports, missing docstrings, and all sorts of mischief. Read them if you dare!

bad_python.py

import os
import sys
import math


class SampleClass:
    def __init__(self, value):
        self.value = value

    def calculate_square(self):
        return self.value ** 2

    def print_square(self):
        print(f"Square of {self.value} is: {self.calculate_square()}")


# Create an instance of SampleClass
sample = SampleClass(4)
sample.print_square()

What’s going on here?

  • imports sys and math, but then never really uses sys.

  • Class SimpleClass is fairly well-structured, but it might annoy some linters if it lacks a docstring.

  • Overall, it’s not horrifying, but it has style quirks and an unused import.

more_bad_python.py

import sys
import os
from datetime import datetime
import json
from collections import deque
import re

def  my_function (arg1,arg2 ):
  result=arg1+arg2
  print("Result:",result)
 return result

class myClass:
 def __init__(self,value):
  self.value=value
 
 def display (self):
  print("Value is",self.value)

x= myClass (10)
 x.display()


 def another_function():
  x=[1,2,3,4,5]
  for i in x:
   print(i)

Our second file has more serious problems:

  • Indentation issues are front and center. my_function’s return statement is aligned improperly, so it might never run.

  • Class naming breaks the usual Python convention: myClass is named in a style that is not the typical CapWords (MyClass).

  • Some lines are out of place: x= myClass(10) is floating at a random indent level.

long_bad_python.py

import sys
import os, json
from math import sqrt, pi
from random import randint
from os import path




def calculate_area(radius):
  area= pi*(radius**2)
  return area



def random_number_generator(min, max):
 return randint(min,max)



def load_json(file_path):
   if path.exists(file_path)== False:
     raise FileNotFoundError('File does not exist')
   else:
    with open(file_path, 'r') as file:
        data=json.load(file)
    return data





def save_json(data,file_path):
    with open(file_path, 'w') as file:
        json.dump(data, file, indent = 4)









class Person:
    def __init__(self,name, age):
        self.name=name
        self.age =age

    def say_hello( self ):
      print(f'Hello, my name is {self.name} and I am {self.age} years old.')









def   main():

  radius = 5
  area = calculate_area(radius)
  print ('The area is:', area)


  random_number=random_number_generator(1, 10)
  print('Random number:',random_number)




  person = Person('John',30)
  person.say_hello()



  data = {'name': 'Alice','age': 25}

  file_path ='data.json'
  save_json(data, file_path)




if __name__=="__main__":
  main()

This is the big daddy of bad python.

  • Imports sqrt but uses only pi.

  • Has a main function with strange spacing: def main():

  • Some extraneous blank lines and messy spacing around operators and parentheses.

  • Inconsistent indentation and random imports from multiple lines.

Start your engines!

Now, let’s sit back and watch how each linter handles this madness:

black

When you run black on these files, it immediately standardizes spacing, line length, and indentation. You will see it remove trailing spaces, replace double quotes with single quotes or vice versa (depending on black’s style decisions), and enforce consistent wrapping of long lines. For example, in long_bad_python.py, black rearranges import statements, pulling multiple imports into separate lines or merging them as it sees fit. black leaves behind a properly indented version of my_function in more_bad_python.py, so you can actually see where the return statement should be. black is not going to warn you about the unused import of sys in bad_python.py, though. It is only about tidying up the shape of your code. After black’s pass, you still might have deeper issues, but at least the code will look consistently formatted.

flake8

Next in line is flake8. On bad_python.py, flake8 quickly warns about sys being an unused import, confirming our suspicion. On more_bad_python.py, flake8 complains that the indentation for return result is off. It also flags the fact that lines might exceed 79 characters if you have typed out print statements that are too long. On long_bad_python.py, you might see flake8 griping about trailing whitespace, inconsistent indentation, and the unused sqrt. But don’t expect it to fix anything. flake8’s job is simply to throw a polite (or sometimes blunt) error message your way, leaving the actual fix to you.

pylint

Where black is carefree and flake8 is concise, pylint is that teacher who grades your every sentence in red ink. Running pylint on bad_python.py shows multiple categories of feedback: naming conventions, missing module docstring, and an unused import for sys. On more_bad_python.py, pylint’s thorough approach reveals that myClass should probably be MyClass by Python’s standard naming guidelines, that my_function is missing docstrings, and that the indentation of x= myClass(10) is suspicious. You may also see a warning about code that is unreachable if the indentation truly breaks the function logic. Meanwhile, long_bad_python.py triggers several design suggestions. For instance, you might receive “missing function docstring” for random_number_generator or main, or an “unused variable” warning for sqrt. If you love data, pylint even assigns a score to your code, letting you know how close to “perfect” your style is. While it can feel like too many warnings, thoroughness pays off if you want to catch subtle issues that might cause bugs.

ruff

Finally, ruff swoops in with blazing speed. Written in Rust, it attempts to cover many flake8, pylint, and other rule sets at once, while being shockingly fast. On more_bad_python.py, ruff flags the indentation errors, complaining about the misaligned return statement. It also yells about the missing newline at the end of files if there is one. In bad_python.py, ruff warns about sys being unused, and in long_bad_python.py, it again points out that sqrt is imported but never used. Ruff can fix some of these issues automatically—unused imports can sometimes be removed with a single ruff command—but it still is not the same as black’s full rewrite. Instead, ruff tries to find a balance between the completeness of pylint and the speed of flake8. Its big selling point is that it can replicate many existing lint rules in one pass, saving time and effort.

Who is our champion?

So, which one should you use? The real answer might be more than one. black to format your code is a time saver, especially on teams. flake8 or ruff can then check for style slip-ups, while pylint is your friend if you want deep feedback on docstrings, naming, and design. Some developers combine black and flake8 or black and ruff in their continuous integration pipelines to keep everything consistent and clean. If you like total thoroughness, add pylint to the mix. Yes, it might feel like the code police moved into your project, but you will probably thank them later.

For more insight into Python linters, check out the official Python docs or see a great overview at Real Python. You can always visit Stack Overflow to get help resolving cryptic linter warnings. There is nothing like reading the stories of others who got stuck on the same obscure error you did.

If all this linting discussion has you hungry for better DevOps practices, open a free Caparra account. It is a great way to continue your learning and get access to tools that help you integrate, deploy, and maintain code in a more automated and consistent way. Your future self (and your code reviewers) will thank you when your Python code runs smoothly and looks great. Have fun refining your code, and may your linting adventures be as clean as possible!

Previous
Previous

How Code Reviews Should Work

Next
Next

Microservices and DevOps: A Perfect Match