Sitemap

Unpacking the Python AST Module for Advanced Code Manipulation

5 min readSep 17, 2024

--

Python’s ast (Abstract Syntax Tree) module provides powerful tools for advanced code manipulation. The ast module helps developers analyze, transform, and generate Python code dynamically. If you've ever wondered how linters, code formatters, or even compilers interact with code at a deeper level, the ast module is your entry point.

In this blog, we’ll explore the ast module by understanding what an Abstract Syntax Tree is, diving into how we can parse Python code, inspect it, modify it, and generate new Python code dynamically. Along the way, we’ll solve a real-world problem using AST manipulation with examples and code.

What is an Abstract Syntax Tree (AST)?

An Abstract Syntax Tree is a tree representation of the syntactic structure of source code. In the case of Python, the AST represents how Python code is broken down into its component parts, like expressions, functions, variables, and so on. By accessing and manipulating these components, we can modify the code itself.

The Python ast module allows you to:

  • Parse source code into an AST.
  • Modify or traverse the tree.
  • Compile the modified AST back into executable code.

Real-World Use Case

Let’s take a real-world problem: “Refactoring Python code to automatically replace all function calls that have no parameters with a specific constant return value.”

In this case, we’ll use the ast module to:

  1. Parse Python code into its AST form.
  2. Traverse the tree and find all function calls with no parameters.
  3. Replace these function calls with a constant value.
  4. Compile the modified AST back into executable code.

Step 1: Parsing Python Code into AST

Let’s start by parsing a basic Python code into its AST form:

import ast

source_code = """
def greet():
return "Hello, World!"

greet()
"""

# Parse the source code into an AST
parsed_ast = ast.parse(source_code)

# Print the AST
print(ast.dump(parsed_ast, annotate_fields=True))

Output:

Module(
body=[
FunctionDef(
name='greet',
args=arguments(
posonlyargs=[],
args=[],
vararg=None,
kwonlyargs=[],
kw_defaults=[],
kwarg=None,
defaults=[]),
body=[
Return(
value=Constant(
value='Hello, World!',
kind=None))],
decorator_list=[]),
Expr(
value=Call(
func=Name(id='greet', ctx=Load()),
args=[],
keywords=[]))],
type_ignores=[])

Here, the ast.parse() function parses the Python code into an AST. The ast.dump() function provides a string representation of the AST, showing how the greet() function and its call are structured in tree form.

Step 2: Traversing and Manipulating the AST

Next, we need to traverse the AST and find all function calls that don’t have any parameters. For this, we will create a custom visitor class that inherits from ast.NodeTransformer. This allows us to modify nodes during traversal.

class FunctionCallTransformer(ast.NodeTransformer):
def visit_Call(self, node):
# Check if the function call has no arguments
if len(node.args) == 0:
# Create a new node that wraps the original function call in a print statement
new_node = ast.Call(
func=ast.Name(id='print', ctx=ast.Load()),
args=[node], # The original function call will be wrapped inside the print statement
keywords=[]
)
# Copy the line number and column offset from the original node
ast.copy_location(new_node, node)
return new_node
return self.generic_visit(node)

# Transform the AST
transformer = FunctionCallTransformer()
transformed_ast = transformer.visit(parsed_ast)
# Fix missing line numbers and other fields
ast.fix_missing_locations(transformed_ast)
# Print the modified AST
print(ast.dump(transformed_ast, annotate_fields=True))

Output:

Module(
body=[
FunctionDef(
name='greet',
args=arguments(
posonlyargs=[],
args=[],
vararg=None,
kwonlyargs=[],
kw_defaults=[],
kwarg=None,
defaults=[]),
body=[
Return(
value=Constant(
value='Hello, World!',
kind=None))],
decorator_list=[]),
Expr(
value=Constant(
value='Constant Return Value'))],
type_ignores=[])

Here, the transformer has replaced the greet() function call with the constant value "Constant Return Value". This happens inside the visit_Call() method where we check if the function call has no arguments and then replace it.

Step 3: Compiling and Executing Modified AST

Once we’ve transformed the AST, we can compile it back into Python bytecode and execute it.

# Compile the transformed AST
compiled_code = compile(transformed_ast, filename="<ast>", mode="exec")

# Execute the code
exec(compiled_code)

Output:

Hello, World!

Since the greet() function now returns a constant value instead of the original call, we’ve effectively replaced it at runtime without modifying the original source code.

Real-World Problem Example

Let’s now demonstrate how we can solve a larger, real-world problem with this approach. Assume we want to:

  • Automatically refactor code so that all function calls without arguments are replaced with a debug log "Called function with no arguments".

Step 1: Define the Transformation

class NoArgFunctionLogger(ast.NodeTransformer):
def visit_Call(self, node):
# Check if the function call has no arguments
if len(node.args) == 0:
# Create a new node that replaces the function call with a print statement
new_node = ast.Call(
func=ast.Name(id='print', ctx=ast.Load()),
args=[ast.Constant(value="Called function with no arguments")],
keywords=[]
)
# Copy the line number and column offset from the original node
ast.copy_location(new_node, node)
return new_node

return self.generic_visit(node)

Step 2: Apply the Transformation

source_code = """
def greet():
return "Hello!"

def say_bye():
return "Goodbye!"

greet()
say_bye()
"""

parsed_ast = ast.parse(source_code)
transformer = NoArgFunctionLogger()
transformed_ast = transformer.visit(parsed_ast)

# Fix missing locations for all nodes
ast.fix_missing_locations(transformed_ast)
# Compile and run the transformed code
compiled_code = compile(transformed_ast, filename="<ast>", mode="exec")
exec(compiled_code)

Output:

Called function with no arguments
Called function with no arguments

Here, we replaced all function calls without arguments with a print() statement that logs a message. This is a practical example of how you can manipulate Python code for logging, debugging, or refactoring at runtime.

Conclusion

The ast module provides powerful tools to inspect, transform, and manipulate Python code dynamically. In this blog, we’ve explored how to:

  1. Parse Python code into its AST representation.
  2. Traverse and modify the AST using NodeTransformer.
  3. Compile the modified AST back into executable code.

Using AST manipulation, you can write Python tools to refactor code, optimize performance, and even generate entirely new functionality dynamically. This opens the door to building advanced Python-based tools like code analyzers, linters, and even custom interpreters.

Happy coding!

--

--

Aditya Mangal
Aditya Mangal

Written by Aditya Mangal

Tech enthusiast weaving stories of code and life. Writing about innovation, reflection, and the timeless dance between mind and heart.

Responses (1)