Free — no signup required

The Abstract Syntax Tree (AST)

2 min read

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

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free