What is the difference between ast.NodeVisitor and ast.NodeTransformer in Python?
answer
- Same dispatch, different return contract
- One reads the tree, one replaces nodes
- Returning nothing is not harmless
- Your visit_ method owns that subtree
- generic_visit is what descends
basics
~10 sast.NodeVisitor reads a tree: its visit_<NodeType> methods return whatever they like and the tree is untouched. ast.NodeTransformer rewrites it: whatever a visit method returns replaces the node, and returning None deletes it.
solid answer
~40 sBoth dispatch on node type - `visit(node)` looks for a method named `visit_` plus the node's class name, such as `visit_Call`, and falls back to `generic_visit`. The difference is what the return value means. With `ast.NodeVisitor` the return value is ignored by the traversal, so it is for collecting: a linter accumulates findings on `self`. With `ast.NodeTransformer` the return value **replaces the node in its parent**: return the node to keep it, a new node to swap it, a list of statements to splice several in place of one, or `None` to remove it. The trap in both is `generic_visit`: the base `visit_` dispatch does **not** descend automatically, so a `visit_X` method that does not call `self.generic_visit(node)` stops the walk at that node and misses everything nested inside it.
code
python · 22 linesimport ast
SOURCE = """
def load(rows):
cache = list(rows)
return list(cache)
"""
class CallCounter(ast.NodeVisitor):
def __init__(self):
self.calls = []
def visit_Call(self, node):
if isinstance(node.func, ast.Name):
self.calls.append((node.func.id, node.lineno))
self.generic_visit(node)
counter = CallCounter()
counter.visit(ast.parse(SOURCE))
print(counter.calls)go deeper
Recall which of the two rewrites and which only reads, and that methods are named visit_ plus the node class, such as visit_Call. Knowing ast.NodeTransformer edits and ast.NodeVisitor inspects is the level bar here.
Explain the return-value contract of a transformer - node, new node, list, or None to delete - and why self.generic_visit(node) is mandatory when you want to reach nested nodes. Expect to write a small visitor on a whiteboard.
Show the failure modes you have hit: silent deletion from a missing return, subtrees skipped by a missing generic_visit, and single-pass traversal not revisiting generated nodes. Argue for separate collect and rewrite passes over a single clever traversal.
Weigh whether hand-rolled AST passes belong in your toolchain at all versus an off-the-shelf analyser, and set the standard for testing them - visitors that silently match nothing are the failure mode that survives review.
## One dispatch mechanism, two contracts `ast.NodeVisitor` and `ast.NodeTransformer` share their dispatch. You call `visitor.visit(tree)`; `visit` looks up a method named `visit_` plus the node's class name - `visit_Call`, `visit_FunctionDef`, `visit_Constant` - and calls it. If no such method exists, `generic_visit` runs instead, and `generic_visit` is the part that actually walks into the node's children. `ast.NodeTransformer` subclasses `ast.NodeVisitor`; it overrides `generic_visit` so that the children it walks are also *replaced* by whatever the child visits return. So the mechanism is the same and the **contract on the return value** is what differs. ## NodeVisitor: read-only, accumulate on self With a plain visitor the return value goes nowhere useful - the traversal ignores it. The idiom is therefore to accumulate state on the instance: ```python class CallCounter(ast.NodeVisitor): def __init__(self): self.calls = [] def visit_Call(self, node): if isinstance(node.func, ast.Name): self.calls.append((node.func.id, node.lineno)) self.generic_visit(node) ``` That is the shape of essentially every hand-written linter rule: match a node type, decide, record a finding with its position, keep walking. ## NodeTransformer: the return value is the edit With a transformer, whatever a `visit_X` method returns takes the old node's place in its parent: - **return `node`** - keep it (possibly after mutating its fields in place); - **return a different node** - substitute it; - **return `None`** - delete the node from its parent's list; - **return a list of nodes** - splice them in, but only where the parent field holds a *list of statements*. Returning a list where a single expression is expected produces a broken tree, not a syntax error, and the failure surfaces later at `compile()`. Deletion is the sharp edge: `None` deletes silently, so a transformer that forgets to return the node on its non-matching branch quietly strips code. Make the last line of every `visit_` method an explicit `return node`. ## The generic_visit trap The single most common bug in both classes is forgetting `self.generic_visit(node)`. Once you define `visit_FunctionDef`, *your method owns that subtree*: nothing below it is visited unless you descend yourself. A rule that flags nested calls will silently report only the outermost one; a transformer will rewrite the top of a nested structure and leave the inside untouched. In a transformer, call `self.generic_visit(node)` **before** you build the replacement if you want the children rewritten first (bottom-up), and remember it returns the node with its children already processed. Related: visiting is single-pass, so an edit that creates a node your own visitor would have matched is *not* revisited unless you run the transformer again over the result. Codemods that must reach a fixed point loop until the tree stops changing. ## Choosing between them Use a visitor when the output is information - findings, a symbol table, a call graph, metrics. Use a transformer when the output is a tree you will hand to `compile()` or `ast.unparse()`. Wanting both usually means two passes: collect first, decide, then rewrite, which is easier to reason about than deciding and editing in one traversal. A detail worth carrying: a rewritten tree is not ready to compile. New nodes you construct have no position fields, and `compile()` rejects that with a `TypeError` until you run `ast.fix_missing_locations()` or `ast.copy_location()` over the result. ## Node types are grammar, and grammar changes Because dispatch is by class name, a visitor method whose name no longer matches any node class simply never fires - it does not raise. Every literal has been an `ast.Constant` since Python 3.8, so handlers named for the old per-literal node classes are dead code on a modern interpreter. In the other direction, `visit_Match` is meaningless before 3.10 and essential after it if your rule must see pattern-matching code. Test that a visitor actually fires; a silent zero is indistinguishable from a clean file. ## Testing a pass Because both classes fail silently - a handler that never fires, a subtree never entered, a node quietly deleted - the test for an AST pass is not "it did not crash". Assert on the collected findings for a fixture that contains a nested, an aliased and a negative case, and for a transformer assert on `ast.unparse()` of the result so the shape of the rewrite is visible in the test. A pass that returns zero findings should be indistinguishable in your suite from one that was never called; if it is not, you have no evidence the rule works.
- What happens if a visit_ method on an ast.NodeTransformer forgets to return the node?It returns `None`, and the transformer takes that as "delete this node", removing the statement or expression from its parent. Nothing warns you: the tree is simply smaller. That is why every `visit_` method should end with an explicit `return node`, and why round-tripping the result through `ast.unparse()` in a test catches the mistake early.
- Why does a rule that flags calls sometimes report only the outermost call in nested code?Because the `visit_Call` method never called `self.generic_visit(node)`. Dispatching to a `visit_` method replaces the default traversal of that node, so children are not visited unless the method descends explicitly. The subtree is skipped silently, which reads as a clean file rather than as an error.
- How would you replace one statement with several using ast.NodeTransformer?Return a list of statement nodes from the `visit_` method. The transformer splices them into the parent's body in place of the original. This only works where the parent field is a list of statements - returning a list where a single expression node is expected builds a structurally invalid tree that fails later at `compile()`.
saying these in an interview costs you the question
- Thinking a visitor can rewrite the tree by returning a node
- Believing children are visited automatically inside visit_X
- Forgetting that returning None deletes the node
- Assuming a transformer re-visits nodes it just created
- Naming visit_ methods for node classes that no longer exist