Comprehensive static code analysis tool for Python that performs deep code inspection without executing the program
—
Framework for creating custom checkers that analyze specific code patterns. Pylint's checker system is built on a flexible architecture that allows developers to create specialized analysis tools for their specific coding standards and requirements.
Note: Pylint checkers work with AST nodes provided by the astroid library (pylint's dependency). References to astroid.Node, astroid.parse(), etc. in the examples below refer to this external library.
Foundation classes that provide the interface and common functionality for all checkers.
class BaseChecker:
"""
Abstract base class for all checkers.
Attributes:
name (str): Unique name identifying the checker
msgs (dict): Message definitions with format: {
'message-id': (
'message-text',
'message-symbol',
'description'
)
}
options (tuple): Configuration options for the checker
reports (tuple): Report definitions
priority (int): Checker priority (lower runs first)
"""
name: str
msgs: dict
options: tuple
reports: tuple
priority: int
def __init__(self, linter=None):
"""
Initialize the checker.
Args:
linter: PyLinter instance this checker belongs to
"""
def open(self):
"""Called before checking begins."""
def close(self):
"""Called after all checking is complete."""Specialized base classes for different types of code analysis.
class BaseRawFileChecker(BaseChecker):
"""
Base class for checkers that process raw file content.
Used for checkers that need to analyze the file as text
rather than parsed AST (e.g., encoding, line length).
"""
def process_module(self, astroid_module):
"""
Process a module's raw content.
Args:
astroid_module: Astroid module node
"""
class BaseTokenChecker(BaseChecker):
"""
Base class for checkers that process tokens.
Used for checkers that analyze code at the token level
(e.g., formatting, whitespace, comments).
"""
def process_tokens(self, tokens):
"""
Process tokens from the module.
Args:
tokens: List of token tuples (type, string, start, end, line)
"""Most checkers inherit from BaseChecker and implement visit methods for AST nodes.
# Example AST checker pattern
class CustomChecker(BaseChecker):
"""Custom checker examining function definitions."""
name = 'custom'
msgs = {
'C9999': (
'Custom message: %s',
'custom-message',
'Description of the custom check'
)
}
def visit_functiondef(self, node):
"""Visit function definition nodes."""
# Analysis logic here
if some_condition:
self.add_message('custom-message', node=node, args=(info,))
def visit_classdef(self, node):
"""Visit class definition nodes."""
pass
def leave_functiondef(self, node):
"""Called when leaving function definition nodes."""
passSystem for defining and managing checker messages with categories and formatting.
# Message format structure
MSG_FORMAT = {
'message-id': (
'message-template', # Template with %s placeholders
'message-symbol', # Symbolic name for the message
'description' # Detailed description
)
}
# Message categories
MESSAGE_CATEGORIES = {
'C': 'convention', # Coding standard violations
'R': 'refactor', # Refactoring suggestions
'W': 'warning', # Potential issues
'E': 'error', # Probable bugs
'F': 'fatal', # Errors preventing further processing
'I': 'info' # Informational messages
}Configuration system for checker-specific options.
# Options format
OPTIONS_FORMAT = (
'option-name', # Command line option name
{
'default': value, # Default value
'type': 'string', # Type: string, int, float, choice, yn, csv
'metavar': '<value>',# Help text placeholder
'help': 'Description of the option',
'choices': ['a','b'] # For choice type options
}
)
# Example options definition
options = (
('max-complexity', {
'default': 10,
'type': 'int',
'metavar': '<int>',
'help': 'Maximum allowed complexity score'
}),
('ignore-patterns', {
'default': [],
'type': 'csv',
'metavar': '<pattern>',
'help': 'Comma-separated list of patterns to ignore'
})
)Functions and patterns for registering checkers with the PyLinter.
def register(linter):
"""
Register checker with linter.
This function is called by pylint when loading the checker.
Args:
linter: PyLinter instance to register with
"""
linter.register_checker(MyCustomChecker(linter))
def initialize(linter):
"""
Alternative registration function name.
Some checkers use this name instead of register().
"""
register(linter)from pylint.checkers import BaseChecker
from pylint.interfaces import IAstroidChecker
class FunctionNamingChecker(BaseChecker):
"""Check that function names follow naming conventions."""
__implements__ = IAstroidChecker
name = 'function-naming'
msgs = {
'C9001': (
'Function name "%s" should start with verb',
'function-name-should-start-with-verb',
'Function names should start with an action verb'
)
}
options = (
('required-function-prefixes', {
'default': ['get', 'set', 'is', 'has', 'create', 'update', 'delete'],
'type': 'csv',
'help': 'Required prefixes for function names'
}),
)
def visit_functiondef(self, node):
"""Check function name starts with approved verb."""
func_name = node.name
prefixes = self.config.required_function_prefixes
if not any(func_name.startswith(prefix) for prefix in prefixes):
self.add_message(
'function-name-should-start-with-verb',
node=node,
args=(func_name,)
)
def register(linter):
"""Register the checker with pylint."""
linter.register_checker(FunctionNamingChecker(linter))from pylint.checkers import BaseTokenChecker
import tokenize
class CommentStyleChecker(BaseTokenChecker):
"""Check comment formatting style."""
name = 'comment-style'
msgs = {
'C9002': (
'Comment should have space after #',
'comment-no-space',
'Comments should have a space after the # character'
)
}
def process_tokens(self, tokens):
"""Process tokens to check comment formatting."""
for token in tokens:
if token.type == tokenize.COMMENT:
comment_text = token.string
if len(comment_text) > 1 and comment_text[1] != ' ':
self.add_message(
'comment-no-space',
line=token.start[0],
col_offset=token.start[1]
)
def register(linter):
linter.register_checker(CommentStyleChecker(linter))from pylint.checkers import BaseRawFileChecker
class FileHeaderChecker(BaseRawFileChecker):
"""Check for required file headers."""
name = 'file-header'
msgs = {
'C9003': (
'Missing required copyright header',
'missing-copyright-header',
'All files should contain a copyright header'
)
}
options = (
('required-header-pattern', {
'default': r'# Copyright \d{4}',
'type': 'string',
'help': 'Regex pattern for required header'
}),
)
def process_module(self, astroid_module):
"""Check module for required header."""
with open(astroid_module.file, 'r', encoding='utf-8') as f:
content = f.read()
import re
pattern = self.config.required_header_pattern
if not re.search(pattern, content[:500]): # Check first 500 chars
self.add_message('missing-copyright-header', line=1)
def register(linter):
linter.register_checker(FileHeaderChecker(linter))from pylint.checkers import BaseChecker
import astroid
class VariableUsageChecker(BaseChecker):
"""Track variable usage patterns."""
name = 'variable-usage'
msgs = {
'W9001': (
'Variable "%s" assigned but never used in function',
'unused-function-variable',
'Variables should be used after assignment'
)
}
def __init__(self, linter=None):
super().__init__(linter)
self._function_vars = {}
self._current_function = None
def visit_functiondef(self, node):
"""Enter function scope."""
self._current_function = node.name
self._function_vars[node.name] = {
'assigned': set(),
'used': set()
}
def visit_assign(self, node):
"""Track variable assignments."""
if self._current_function:
for target in node.targets:
if isinstance(target, astroid.AssignName):
self._function_vars[self._current_function]['assigned'].add(
target.name
)
def visit_name(self, node):
"""Track variable usage."""
if self._current_function and isinstance(node.ctx, astroid.Load):
self._function_vars[self._current_function]['used'].add(node.name)
def leave_functiondef(self, node):
"""Check for unused variables when leaving function."""
if self._current_function:
vars_info = self._function_vars[self._current_function]
unused = vars_info['assigned'] - vars_info['used']
for var_name in unused:
self.add_message(
'unused-function-variable',
node=node,
args=(var_name,)
)
self._current_function = None
def register(linter):
linter.register_checker(VariableUsageChecker(linter))from pylint.testutils import CheckerTestCase, MessageTest
class TestFunctionNamingChecker(CheckerTestCase):
"""Test cases for function naming checker."""
CHECKER_CLASS = FunctionNamingChecker
def test_function_with_verb_prefix(self):
"""Test that functions with verb prefixes pass."""
code = '''
def get_value():
pass
def create_user():
pass
'''
with self.assertNoMessages():
self.walk(astroid.parse(code))
def test_function_without_verb_prefix(self):
"""Test that functions without verb prefixes fail."""
code = '''
def value(): # Should trigger warning
pass
'''
message = MessageTest(
'function-name-should-start-with-verb',
node='value',
args=('value',)
)
with self.assertAddsMessages(message):
self.walk(astroid.parse(code))Install with Tessl CLI
npx tessl i tessl/pypi-pylint