Python ast: Analyze and Transform Code

Published on: August 27, 2026
Reading time: 6 minutes
A developer typing code on a laptop with a Python book beside in an office.

The ast module converts Python source code into an abstract syntax tree. Instead of treating a program as plain text, a tool works with nodes representing modules, functions, classes, calls, operators, names, literals, and control-flow structures. Linters, formatters, security scanners, automated migrations, documentation generators, and educational tools use this structural layer.

An AST describes syntax, not the complete runtime behavior of a program. Dynamic imports, reflection, monkey patching, descriptors, metaclasses, and data-dependent execution limit what static analysis can prove. Compiling or executing a modified tree also runs code with the same risks as any other Python program.

Parse source text

ast.parse() accepts source code and returns a Module node.

import ast

source = "result = add(2, 3)"
tree = ast.parse(source, filename="example.py", mode="exec")
print(type(tree).__name__)

The filename appears in errors and tracebacks. Supply the real path when known.

exec, eval, and single modes

mode="exec" parses a module containing statements. eval accepts one expression. single represents interactive input.

expression = ast.parse("1 + 2 * 3", mode="eval")

Using the wrong mode produces SyntaxError or a tree incompatible with the intended compilation mode.

Inspect with dump

ast.dump() creates a readable representation of a tree.

print(ast.dump(tree, indent=2, include_attributes=True))

include_attributes=True includes line and column positions, which are valuable for diagnostics and source editing.

Basic node structure

A module contains a body list. An assignment is an Assign node, a function call is Call, identifiers are Name, and ordinary literals are commonly represented by Constant.

assignment = tree.body[0]
print(type(assignment).__name__)
print(type(assignment.value).__name__)

Real tools should not depend on fixed list indexes. Traverse the tree and validate node types.

Load, Store, and Del context

Name, Attribute, and Subscript nodes carry a context showing whether the expression is being read, assigned, or deleted.

for node in ast.walk(ast.parse("x = y + 1")):
    if isinstance(node, ast.Name):
        print(node.id, type(node.ctx).__name__)

This distinction is essential for tools that track definitions and uses.

Walk a tree

ast.walk() yields a node and all descendants without offering contextual entry and exit hooks.

calls = [
    node for node in ast.walk(tree)
    if isinstance(node, ast.Call)
]

It is convenient for simple searches. Use visitors when traversal order and scope matter.

NodeVisitor

Subclass ast.NodeVisitor and define methods named visit_NodeType.

class FunctionCollector(ast.NodeVisitor):
    def __init__(self):
        self.names = []

    def visit_FunctionDef(self, node):
        self.names.append(node.name)
        self.generic_visit(node)

collector = FunctionCollector()
collector.visit(ast.parse(source_code))
print(collector.names)

Call generic_visit() when child nodes should also be visited. Omitting it stops traversal below that node.

Async functions

AsyncFunctionDef differs from FunctionDef. A function collector should handle both.

def visit_AsyncFunctionDef(self, node):
    self.names.append(node.name)
    self.generic_visit(node)

The same caution applies to async comprehensions, AsyncFor, and AsyncWith.

Classes and scopes

ClassDef, functions, lambdas, and comprehensions create different scope rules. Merely collecting names does not resolve lexical binding.

For symbol analysis, combine the AST with symtable and maintain an explicit stack of scopes.

Source positions

Many nodes contain lineno, col_offset, end_lineno, and end_col_offset.

for node in ast.walk(tree):
    if isinstance(node, ast.Call):
        print(node.lineno, node.col_offset, node.end_lineno, node.end_col_offset)

Column offsets require care when mapping parser positions to user interfaces containing Unicode text.

Recover source segments

ast.get_source_segment(source, node) returns the corresponding text when position data exists.

segment = ast.get_source_segment(source, calls[0])

The segment preserves that text, but an AST does not retain every comment and formatting decision needed for perfect round trips.

Comments and tokens

Ordinary comments are not AST nodes. Tools that need to preserve comments, whitespace, quote style, and exact formatting should use tokens or a concrete syntax tree.

The AST is designed for structural meaning, not byte-for-byte source preservation.

Safe literal parsing with literal_eval

ast.literal_eval() accepts supported literal structures such as strings, bytes, numbers, tuples, lists, dictionaries, sets, booleans, and None.

settings = ast.literal_eval("{'retries': 3, 'enabled': True}")

It is much narrower than eval(), but should not receive enormous or deeply nested hostile input. Such values can consume memory, CPU, or stack depth.

Do not use eval for untrusted code

ast.parse() does not execute code, but compile(), exec(), and eval() will execute a valid tree. Rejecting a few node types does not automatically produce a secure sandbox.

Python offers many indirect ways to reach objects and perform operations. Use process isolation, resource limits, and a truly restricted language for hostile input.

Transform with NodeTransformer

NodeTransformer allows a visitor method to replace a node by returning another node.

class Rename(ast.NodeTransformer):
    def visit_Name(self, node):
        if node.id == "old_name":
            return ast.copy_location(
                ast.Name(id="new_name", ctx=node.ctx),
                node,
            )
        return node

tree = Rename().visit(tree)
ast.fix_missing_locations(tree)

Preserve the original context and copy locations for useful errors and debugging.

Remove or expand statements

A transformer can return None to remove a node from a statement list, or return a list to replace one statement with several. Not every field accepts those forms.

Compile and test every transformed tree.

fix_missing_locations

Manually created nodes may not include line information. fix_missing_locations() fills missing values from parent nodes.

This makes compilation possible but does not automatically create editorially perfect positions.

copy_location and increment_lineno

copy_location(new, old) copies source positions. increment_lineno() shifts line numbers, which can help when adding a generated prefix.

Useful tracebacks depend on coherent filenames and locations.

Generate code with unparse

ast.unparse() produces Python source from a tree.

new_source = ast.unparse(tree)

The output may change quotes, parentheses, whitespace, and layout. It aims for syntactic equivalence, not textual preservation.

Compile an AST

compile() accepts a valid AST when required fields and locations are present.

code_object = compile(tree, "transformed.py", "exec")
namespace = {}
exec(code_object, namespace)

Execute only trusted code and control the namespace. A limited globals dictionary reduces accidental exposure but does not create a sandbox.

Grammar versions

The tree shape changes as Python syntax evolves. Tools supporting several interpreter versions must test each target and avoid assuming that every node type exists everywhere.

feature_version can request an approximation of an older grammar within the current parser’s capabilities, but it does not replace running tests on the target version.

Type comments and annotations

Parsing can retain selected type comments, while modern annotations appear in annotation nodes on arguments, assignments, and definitions.

For complete type semantics, use a type checker or its API rather than attempting to infer everything from the AST alone.

Decorators

Functions and classes have a decorator_list. Decorators are executed expressions and can replace or substantially modify the object being defined.

A static analyzer should report uncertainty instead of assuming that a decorated function preserves its original behavior.

Searching for dangerous calls

A linter may look for patterns involving eval, exec, shell-enabled subprocesses, or unsafe deserialization. Comparing only text names produces both false positives and false negatives.

Aliases, imports, shadowing, and reassignment require symbol resolution, and static analysis still cannot guarantee runtime safety.

Imports

Import and ImportFrom expose declared modules and aliases.

for node in ast.walk(tree):
    if isinstance(node, ast.ImportFrom):
        print(node.module, [alias.name for alias in node.names])

Dynamic and conditional imports require separate treatment.

Complexity metrics

Tools can count branches, loops, exception handlers, comprehensions, and boolean operations to estimate complexity. Metrics are review signals, not proof of quality.

Document the algorithm and keep results stable across supported versions.

Analyze many files

Discover files, read them with the correct encoding, parse each independently, and aggregate diagnostics. One syntax error should not erase results from the rest of the project.

Batch analysis can use bounded concurrency; see Python concurrent.futures. Avoid transferring huge trees between processes when compact diagnostics are sufficient.

Python source encoding

Use tokenize.open() when reading Python files so declared source encodings are respected. Assuming UTF-8 can fail on legacy projects.

Syntax errors

ast.parse() raises SyntaxError. Record filename, line, offset, and message without aborting the whole batch.

try:
    tree = ast.parse(source, filename=path)
except SyntaxError as error:
    report(path, error.lineno, error.offset, error.msg)

Recursion and huge input

Very deep trees can hit recursion limits in visitors and transformations. Huge files also consume substantial memory.

Set operational limits, handle failures, and do not raise the recursion limit without understanding native-stack risk.

Test transformations

Compare behavior before and after transformation, compile the tree, run tests, and inspect generated code. Include comprehensions, async syntax, pattern matching, decorators, f-strings, and annotations.

Use negative cases to verify that a rename does not affect the wrong scope.

Common mistakes

Common failures include forgetting generic_visit(), ignoring AsyncFunctionDef, losing Load/Store context, creating nodes without positions, expecting comments to survive, using literal_eval() without resource limits, executing untrusted trees, and assuming an AST resolves dynamic behavior.

Conclusion

ast turns Python code into a navigable and transformable structure. Use visitors for analysis, transformers for changes, locations for diagnostics, and unparse() when semantic equivalence is enough.

Apply resource limits, test every supported Python version, and treat results as static analysis rather than absolute truth. Consult the official ast documentation and the symtable documentation.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Rustic exposed brick wall featuring aged electrical sockets and metal conduit.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python socket: Build TCP and UDP Networks

    Learn Python socket for TCP and UDP clients and servers, framing, timeouts, IPv6, concurrency, TLS, and network security.

    Ler mais

    Tempo de leitura: 6 minutos
    27/08/2026
    Hands typing code on a laptop in a workspace. Indoor setting focused on software development.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python multiprocessing: Use Multiple Cores

    Learn Python multiprocessing with processes, pools, queues, pipes, shared memory, cancellation, security, and correct shutdown.

    Ler mais

    Tempo de leitura: 7 minutos
    26/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    concurrent.futures: Threads and Processes in Parallel

    Learn Python concurrent.futures with threads, processes, Future objects, timeouts, cancellation, backpressure, and deadlock prevention.

    Ler mais

    Tempo de leitura: 6 minutos
    26/08/2026
    Blurry spinning vinyl record with needle on turntable, capturing the essence of analog music.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python winsound: Play Audio on Windows

    Learn Python winsound to play WAV files, system sounds, beeps, loops, and asynchronous notifications safely on Windows.

    Ler mais

    Tempo de leitura: 6 minutos
    26/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python winreg: Manage Windows Registry

    Learn Python winreg to read and write the Windows Registry, manage types, permissions, WOW64 views, deletion, migrations, and security.

    Ler mais

    Tempo de leitura: 6 minutos
    26/08/2026
    Detailed shot of a Jungle Carpet Python (Morelia spilota cheynei) in its natural habitat.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python posix: Direct Unix System Calls

    Understand Python posix, Unix calls, descriptors, permissions, processes, security, and why most programs should use os instead.

    Ler mais

    Tempo de leitura: 6 minutos
    26/08/2026