Custom Pylint AST Checkers in Python Explained

Pylint analyzes Python source code by transforming it into an Abstract Syntax Tree (AST) via its sister library, astroid. This article explains how Pylint's AST inspection mechanism functions and how developers can utilize this underlying architecture to write custom checkers. By leveraging the visitor pattern, inspecting structural node attributes, and utilizing astroid's advanced inference capabilities, teams can enforce domain-specific coding standards, detect subtle bugs, and automate compliance rules tailored to their own codebases.

The Role of Astroid in Pylint's Analysis

Unlike Python’s built-in ast module, Pylint relies on astroid to parse source code. astroid extends the standard AST by adding static type inference, parent-child node references, and cross-module tracking. When Pylint executes, it parses the target file into a hierarchical tree of AST nodes, where each component of the program—such as modules, classes, functions, and expressions—is represented as an individual node object. Custom checkers rely directly on this enriched tree to evaluate code semantics rather than relying on brittle raw text matching or regular expressions.

The Checker Architecture and the Visitor Pattern

Custom checkers are created by subclassing pylint.checkers.BaseChecker and implementing the pylint.interfaces.IAstroidChecker interface. Pylint utilizes the visitor design pattern to traverse the syntax tree. When Pylint walks the AST, it automatically looks for methods on registered checkers that match the pattern visit_<nodename>() and leave_<nodename>().

For example, to inspect function declarations, a checker defines a visit_functiondef(self, node) method. As Pylint walks the tree, it invokes this method whenever it encounters a FunctionDef node, passing the specific node as an argument.

Inspecting AST Node Properties

Within visitor methods, the checker has full access to the properties of the node being inspected. Node objects expose critical structural details:

Emitting Messages and Reporting Violations

Checkers declare custom messages using a dictionary assigned to the msgs attribute. Each message requires a unique symbol, an ID code, a description, and an explanation.

When a visitor method detects an AST pattern that violates a defined rule, it calls self.add_message():

from pylint.checkers import BaseChecker
from pylint.interfaces import IAstroidChecker

class DisallowPrintChecker(BaseChecker):
    __implements__ = IAstroidChecker

    name = "disallow-print"
    msgs = {
        "E9901": (
            "Direct call to print() detected; use a logger instead.",
            "disallow-print",
            "Disallows explicit calls to the built-in print function in production code.",
        ),
    }

    def visit_call(self, node):
        if hasattr(node.func, "name") and node.func.name == "print":
            self.add_message("disallow-print", node=node)

def register(linter):
    linter.register_checker(DisallowPrintChecker(linter))

Passing the node object directly to self.add_message() allows Pylint to automatically extract the precise line number, column offset, and file path of the violation.

Registering and Executing Custom Checkers

To execute custom checks, Pylint looks for a top-level register(linter) function within the module. This function registers the checker instance with the Pylint engine. Users can then include the custom checker during execution by passing the plugin file or module path to the --load-plugins CLI flag or by configuring the load-plugins setting within a .pylintrc or pyproject.toml configuration file.