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:
- Node Attributes: A
FunctionDefnode provides direct access tonode.name,node.args,node.decorators, andnode.body. - Hierarchical Navigation: Nodes maintain references
to their parent containers via
node.parent, allowing checkers to assess the context of a statement (e.g., verifying if a variable assignment occurs inside a specific class or method). - Type and Value Inference:
astroidnodes implement an.infer()method. This enables checkers to resolve the potential types or runtime values of variables, functions, or returns without executing the code.
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.