A Python AST (abstract syntax tree) is the structured, tree form of your code that Python builds before compiling it to bytecode. The standard-library ast module lets you parse source into that tree (ast.parse), inspect it (ast.dump, ast.walk, NodeVisitor), rewrite it (NodeTransformer), and turn it back into source (ast.unparse). It's how linters, formatters, and obfuscators understand and change code reliably — instead of fragile text or regex hacks.
If you've ever wanted to analyse or rewrite Python code — find every hard-coded string, rename variables, enforce a lint rule, or transform a program — doing it with string search and regex is a losing battle. The right tool is the AST: the tree Python itself builds from your source. Once you work on the tree instead of the text, "find every string literal" or "rename this everywhere" becomes a few lines.
This is a hands-on tour of the standard-library ast module — parse, inspect, visit, and transform — with real, runnable output from Python 3.14. Every snippet below was actually executed. Here's the tiny program we'll analyse throughout:
What an AST actually is
When Python runs your file it doesn't execute the text. It first tokenizes it, then parses those tokens into an abstract syntax tree — a hierarchy of typed nodes (Module, FunctionDef, Assign, Call, Constant…) — and finally compiles that tree to bytecode. The ast module hands you that exact tree.
import osAPI_KEY = "sk-live-abc123"def greet(name):msg = f"hello {name}"return msg.upper()print(greet("mithun"))
Parse and dump: see the tree
ast.parse(source) returns the tree; ast.dump(node, indent=2) prints it. Here's the greet function as the parser sees it — note the f-string becomes a JoinedStr of a constant plus a FormattedValue:
FunctionDef(name='greet',args=arguments(args=[arg(arg='name')]),body=[Assign(targets=[Name(id='msg', ctx=Store())],value=JoinedStr(values=[Constant(value='hello '),FormattedValue(value=Name(id='name', ctx=Load()),conversion=-1)])),Return(value=Call(func=Attribute(value=Name(id='msg', ctx=Load()),attr='upper')))])
Every node carries its children and, usually, its lineno/col_offset. That structure is the whole advantage: you're no longer guessing where a string ends or whether upper is a method call — the tree tells you exactly.
Walk it: find things
ast.walk(tree) yields every node, in no particular order — perfect for "find all X" tasks. Every string and number literal is a Constant node, so pulling every string out of a file is one comprehension:
strings = [n.value for n in ast.walk(tree)if isinstance(n, ast.Constant) and isinstance(n.value, str)]# real output:['sk-live-abc123', 'hello ', 'mithun']
Notice it found the hard-coded sk-live-abc123 — this is exactly how a secret-scanner (or an obfuscator deciding what to encrypt) locates string literals: it reads the tree, not the text.
Visit it: NodeVisitor
For anything beyond a flat search, subclass ast.NodeVisitor and define visit_<NodeType> methods. The visitor dispatches each node to the matching method. Here's one that reports every function definition and call site with its line number:
class V(ast.NodeVisitor):def visit_FunctionDef(self, node):args = [a.arg for a in node.args.args]print(f"def {node.name}({', '.join(args)}) @ line {node.lineno}")self.generic_visit(node)def visit_Call(self, node):name = getattr(node.func, 'id', getattr(node.func, 'attr', '?'))print(f"call {name}() @ line {node.lineno}")self.generic_visit(node)V().visit(tree)
def greet(name) @ line 5call upper() @ line 7call print() @ line 9call greet() @ line 9
Always call self.generic_visit(node) if you want the walk to continue into a node's children. This is the pattern real linters use to implement rules like "no bare except" or "function too long."
Transform it: NodeTransformer + ast.unparse
To *change* code, use ast.NodeTransformer — its visit_* methods return a node to replace the original. Then ast.unparse(tree) (Python 3.9+) turns the modified tree back into source. Here's a transformer that redacts every string literal, and the source it produces:
class Redact(ast.NodeTransformer):def visit_Constant(self, node):if isinstance(node.value, str):return ast.copy_location(ast.Constant(value='<redacted>'), node)return nodenew = ast.fix_missing_locations(Redact().visit(ast.parse(SRC)))print(ast.unparse(new))
import osAPI_KEY = '<redacted>'def greet(name):msg = f'<redacted>{name}'return msg.upper()print(greet('<redacted>'))
Two gotchas: call ast.fix_missing_locations after building new nodes (so they get line numbers to compile/unparse), and remember ast.unparse reformats — it round-trips *meaning*, not exact whitespace or your original quote style.
This redact-and-unparse loop is, in miniature, exactly how an obfuscator works: it parses to an AST, rewrites nodes (rename identifiers, encrypt string constants, restructure), and unparses the result. Our Python Obfuscator does this at scale — see AST-based obfuscation and protecting Python source code.
Where you'll use this
- Linters & formatters (Ruff, Black, flake8) parse to an AST to apply rules and reprint code — never regex.
- Obfuscators & minifiers rewrite the tree (rename, encrypt, strip) then unparse.
- Codemods & migrations (e.g. library API upgrades) transform many files reliably via
NodeTransformer. - Security scanners walk the tree to flag dangerous calls or hard-coded secrets.
- Analysis — count complexity, find dead code, map imports.
The fastest way to build intuition is to *see* the tree for your own code. Paste a snippet into our free Python AST Viewer to explore the node structure interactively, and run experiments in the online Python compiler.
See your code as a tree — free
Paste any Python snippet into the AST Viewer and explore its node structure interactively. In your browser, no signup.
Open the Python AST ViewerFree tools mentioned here
Frequently asked questions
What is a Python AST?
An AST (abstract syntax tree) is the structured, tree-shaped representation of Python source code that the interpreter builds after tokenizing and before compiling to bytecode. Each node is a typed construct — a function definition, an assignment, a call, a constant — with its children and usually its line number. The standard-library ast module gives you access to this tree to inspect or transform code.
How do I parse Python code into an AST?
Call ast.parse(source) with your code as a string; it returns a Module node you can inspect or modify. Use ast.dump(node, indent=2) to print the tree, ast.walk(tree) to iterate every node, and ast.unparse(tree) (Python 3.9+) to turn a tree back into source.
What is the difference between ast.NodeVisitor and ast.NodeTransformer?
NodeVisitor is read-only: you define visit_<NodeType> methods to inspect nodes (for analysis or linting). NodeTransformer is for editing: its visit_* methods return a node to replace the original, so you can rewrite the tree. After transforming, call ast.fix_missing_locations and then ast.unparse to get source back.
How do I find all string literals or function names in Python code?
Parse to a tree and walk it: strings and numbers are ast.Constant nodes (check isinstance(n.value, str)), and functions are ast.FunctionDef nodes (use node.name). ast.walk(tree) yields every node, so a one-line comprehension collects all of a given kind — this is how secret-scanners and linters work.
Can I convert a Python AST back into source code?
Yes. ast.unparse(tree), added in Python 3.9, regenerates valid Python source from a (possibly modified) tree. It preserves the meaning of the code but reformats it — whitespace, quote style, and parenthesization may differ from the original. For older versions you'd need a third-party library like astor.
Is manipulating the AST how Python obfuscators work?
Yes. AST-based obfuscators parse source to a tree, rewrite nodes (rename identifiers, encrypt string constants, flatten control flow), then unparse the result back to valid Python. Working on the tree is why the transformation stays correct where text/regex edits would break the code.