Using Python's ast Module for Code Analysis
Learn how to parse, traverse, and modify Python code with the built-in ast module. Includes real examples for finding hardcoded values, custom linters, and safe tree transformations.
Peeking Under the Hood: Using Python's ast Module for Code Analysis
Have you ever wondered how tools like linters, formatters, or even Python itself understand your code? The answer lies in something called an Abstract Syntax Tree, or AST. Today, I want to show you how to use Python's built-in ast module to analyze Python code programmatically. This is one of those tools that, once you get comfortable with, opens up a whole new world of possibilities.
What Exactly Is an AST?
Think of an AST as a simplified map of your code. When Python parses a file, it doesn't just read it line by line - it builds a tree structure that represents the logic. Each node in this tree corresponds to a piece of your code: a function definition, a variable assignment, a loop, or even a single expression.
The ast module gives us direct access to this tree. Instead of writing the code ourselves, we can have Python parse it and hand us back this structured representation. Pretty neat, right?
Getting Started with ast.parse
Let's start with the basics. The main function you'll use is ast.parse(). Give it a string of Python code, and it returns the root of the AST.
import ast
code = """
def greet(name):
return f"Hello, {name}!"
x = 42
if x > 10:
print(greet("Pythonskillset"))
"""
tree = ast.parse(code)
print(ast.dump(tree, indent=2))
When you run this, you'll see something like a tree structure printed out. Each FunctionDef, Assign, If - these are all nodes. The dump function is perfect for exploration, especially when you're learning.
Walking Through the Tree
Manual examination is fine for tiny snippets, but real codebases are massive. You'll need to walk the tree efficiently. That's where AST visitors come in.
The ast.NodeVisitor class lets you define what happens when you encounter specific node types. For example, let's find all function definitions in a file:
import ast
class FunctionFinder(ast.NodeVisitor):
def __init__(self):
self.functions = []
def visit_FunctionDef(self, node):
self.functions.append(node.name)
self.generic_visit(node) # Don't forget this - it continues the traversal
code = """
def add(a, b):
return a + b
class Calculator:
def multiply(self, x, y):
return x * y
"""
tree = ast.parse(code)
finder = FunctionFinder()
finder.visit(tree)
print(finder.functions) # ['add', 'multiply']
Notice how generic_visit(node) is called inside the visitor method. This is crucial - it tells the visitor to continue traversing the children of that node. Without it, you'd stop at the FunctionDef and never see the multiply method inside the class.
Real-World Example: Finding Hardcoded Values
Here's something practical. Imagine you're reviewing code and want to find all hardcoded numeric literals. Maybe you want to ensure configuration values aren't scattered throughout the code.
import ast
class HardcodedValueFinder(ast.NodeVisitor):
def __init__(self):
self.values = []
def visit_Constant(self, node):
if isinstance(node.value, (int, float)):
self.values.append((node.lineno, node.value))
self.generic_visit(node)
code = """
total_cost = 1450
tax_rate = 0.08
discount = 0
"""
tree = ast.parse(code)
finder = HardcodedValueFinder()
finder.visit(tree)
print(finder.values) # [(2, 1450), (3, 0.08), (4, 0)]
Modifying the AST (Carefully!)
Reading is one thing, but modifying? That's where things get interesting. The ast.NodeTransformer lets you modify the tree structure. You could write a tool that automatically adds logging, changes variable names, or even rewrites code patterns.
Here's a simple example that replaces all occurrences of the number 42 with 0:
import ast
class ReplaceFortyTwo(ast.NodeTransformer):
def visit_Constant(self, node):
if node.value == 42:
return ast.Constant(value=0)
return node
code = "answer = 42"
tree = ast.parse(code)
transformer = ReplaceFortyTwo()
new_tree = transformer.visit(tree)
# Convert back to code
new_code = ast.unparse(new_tree)
print(new_code) # answer = 0
But be careful! Modifying the AST incorrectly can break your code. Always validate the modified tree before writing it back.
Practical Applications
You might be wondering when you'd use this in real life. At Pythonskillset, we've seen teams use the ast module for:
- Custom linters - Enforcing project-specific rules that standard tools don't cover
- Code migration - Automatically updating legacy patterns to modern equivalents
- Static analysis - Extracting metrics like function complexity or dependency graphs
- Testing - Generating test cases automatically by analyzing function signatures
Common Pitfalls to Avoid
The ast module is powerful, but it has its quirks:
- Variable tracing is hard. The AST doesn't track variable values through your program. To know what a variable holds at a given point, you'd need something like a static analyzer.
- Not all Python features are supported. Wildly dynamic code (like using
execoreval) won't show in the AST. - Changes need to be safe. When using
NodeTransformer, always make sure you're not breaking the code's syntax. The modified tree might parse differently.
Wrapping Up
The ast module is one of those Python tools that feels like a superpower once you understand it. It lets you analyze and manipulate Python code as data, opening doors to automation that would otherwise be nearly impossible.
Start small - maybe write a script that counts how many times print() is called in your project. Then work your way up to more complex analyses. And remember, the ast module is your friend, not something to be afraid of.
Next time you're debugging a tricky codebase, or wondering how tools understand your code, you'll know the secret: it's all trees, all the way down.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.