Code as a Data Structure
When Python reads your source file, it doesn't execute it character by character. It first parses it — converting the text into a tree of objects called nodes, where each node represents one logical unit of the program. This tree is the Abstract Syntax Tree.
"Abstract" means the tree captures the logical structure of the code, not its literal text. Whitespace, comments, and formatting are discarded. What remains is pure meaning.
Consider this single line:
x = 5 + 10
To Python's parser, this is not a string. It is a hierarchy of objects:
Module
└── Assign
├── targets: [Name(id='x', ctx=Store)]
└── value: BinOp
├── left: Constant(value=5)
├── op: Add()
└── right: Constant(value=10)
Every element has a type (Assign, BinOp, Constant) and attributes (id, value, op). The tree also records line numbers and column offsets, so you can report exactly where in the source file a rule was violated.
Python exposes this machinery through its built-in ast module. You can inspect any piece of code's tree with ast.dump():
import ast
source = "x = 5 + 10"
tree = ast.parse(source)
print(ast.dump(tree, indent=2))
Module(
body=[
Assign(
targets=[
Name(id='x', ctx=Store())],
value=BinOp(
left=Constant(value=5),
op=Add(),
right=Constant(value=10)))],
type_ignores=[])
This output is your map. Every node type you see here is something you can target in a linter rule.
The NodeVisitor Pattern
The ast module provides a class called ast.NodeVisitor. You subclass it and define methods named visit_<NodeType>. When you call visitor.visit(tree), the visitor walks every node in the tree and calls the matching method if one exists.
import ast
class CallFinder(ast.NodeVisitor):
def visit_Call(self, node):
print(f"Function call on line {node.lineno}")
self.generic_visit(node) # Continue walking child nodes
The call to self.generic_visit(node) is critical — it tells the visitor to keep descending into child nodes. Without it, the walk stops at the first match. For example, if you have a function call inside another function call (print(eval(x))), you need generic_visit to find both.
The most commonly targeted node types for linting are:
| Node Type | What it represents |
|---|---|
Call |
Any function call: foo(), bar(x) |
FunctionDef |
A def statement |
AsyncFunctionDef |
An async def statement |
Import / ImportFrom |
import os / from os import path |
Attribute |
Attribute access: obj.method |
Name |
A variable or function name used in an expression |
Assign |
An assignment statement |
ClassDef |
A class definition |