The following code generates duplicate ast.Load object. Why and how to fix?

code = r"""
def func():
    var = 5

var2 = 10
print(var2)
"""
tree = ast.parse(code)
for node in walk(tree):
    print(node)

The output are:


<ast.Load object at 0x0000023962FB5090>
<ast.Load object at 0x0000023962FB5090>

Is there a way to distinguish them?

These are the same object. One comes from the Name(id='print', ctx=Load()) node and another from Name(id='var2', ctx=Load()), so the way to distinguish them would be based on the parent node that contains them. (Also, ast.dump may be helpful for debugging rather than directly printing the node object). Here is where the docs describe the valid ctx values are (Load, Store, and Del).

so if I’m understanding this correctly, the id is being recycled.

import ast
code = r"""
def func():
    var = 5
 
var2 = 10
print(var2)
"""
tree = ast.parse(code)
record = []
for node in ast.walk(tree):
    record.append(node)
    print(node)

should prevent that happening.

But running on Python 3.14, I see output

Module(body=[FunctionDef(name='func', args=arguments(posonlyargs=[], args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), body=[Assign(targets=[Name(...)], value=Constant(...), type_comment=None)], decorator_list=[], returns=None, type_comment=None, type_params=[]), ..., Expr(value=Call(func=Name(...), args=[Name(...)], keywords=[]))], type_ignores=[])
FunctionDef(name='func', args=arguments(posonlyargs=[], args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), body=[Assign(targets=[Name(id='var', ctx=Store(...))], value=Constant(value=5, kind=None), type_comment=None)], decorator_list=[], returns=None, type_comment=None, type_params=[])
Assign(targets=[Name(id='var2', ctx=Store())], value=Constant(value=10, kind=None), type_comment=None)
Expr(value=Call(func=Name(id='print', ctx=Load(...)), args=[Name(id='var2', ctx=Load(...))], keywords=[]))
arguments(posonlyargs=[], args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[])
Assign(targets=[Name(id='var', ctx=Store())], value=Constant(value=5, kind=None), type_comment=None)
Name(id='var2', ctx=Store())
Constant(value=10, kind=None)
Call(func=Name(id='print', ctx=Load()), args=[Name(id='var2', ctx=Load())], keywords=[])
Name(id='var', ctx=Store())
Constant(value=5, kind=None)
Store()
Name(id='print', ctx=Load())
Name(id='var2', ctx=Load())
Store()
Load()
Load()

so I don’t know how you’re even getting your output.

import ast
code = r"""
def func():
    var = 5
 
var2 = 10
print(var2)
"""
tree = ast.parse(code)
prev = None
for node in ast.walk(tree):
    assert node is not prev
    prev = node
    print(node)

is also guaranteed to run I believe

I am running on Python 3.13.5

I had tried the ‘prev=None’ and ‘record=[]’ both but still get the same output. Maybe version >=3.14 is the only solution.

Think of them as enum constants from before they existed in Python.

Yes, there are two related ast.Name objects with their ctx points to the same ast.Load object. So, it’s hard to say which ast.Name() visits, for example, the last ast.Load object.

I had tried on 3.14.0 and get the same output as yours:-)

Maybe you could describe what you are trying to do more? I suspect instead of using ast.walk, you want to use a ast.NodeVisitor or a ast.NodeTransformer. These give you more flexibility, for instance instead of visiting every node type, you can “visit” only the Name types, and inspect the ctx child node.

I just want to know which ast.Load() is of which ast.Name().
Yes, ast.walk is not suitable for this purpose because it walks through all nodes in no specified order. Referring to the code there, it seems has the output clearer.

class MyVisitor(ast.NodeVisitor):
    def generic_visit(self, node):
        print(f'entering {node.__class__.__name__}')
        super().generic_visit(node)
        print(f'leaving {node.__class__.__name__}')
visitor = MyVisitor()
visitor.visit(tree)

That’s not possible. There is only a single ast.Load() instance that is shared by all it’s parents.

This is an X-Y problem. Why do you think you want to differentiate between different Load instances?

They are different nodes in a tree. Want to know which node it belongs to.

It doesn’t belong to either. The same way the singleton None doesn’t belong to anything else.

Again: why do you think you want to know which Node it belongs to? What do you want to do with this information?

If it’s pure curiosity, the answer is that what you want to do is impossible because that information just doesn’t exists.

My first question comes from that I want to draw the tree manually from the output of ast.walk(tree). Now I am curious how to do something at a particular ast.Load() node.
It’s impossible doing it through ast.walk(). Maybe using NodeVisitor to walk through and checking its parent node and hoping the next following node is the one I want.

I suspect that ast.walk() is the wrong tool for this job. (You’ve seen the docs because you quoted the “no specified order” part; it also says that this is useful only if you “don’t care about the context”.) Have you considered starting with an AST node and directly examining its children? Or possibly building a NodeVisitor?

Yes, building a NodeVisitor is the right direction. I test the following code and it seems works.

class MyVisitor(ast.NodeVisitor):
    hit = False
    def generic_visit(self, node):
        if self.hit:  self.hit=False;  print('do something!')
        if isinstance(node, ast.Name) and node.id == 'print':
            print(f"entering {node}, Name:{repr(node.id)} has ast.{type(node.ctx).__name__} object {node.ctx}")

            self.hit = True
        else:
            print(f'entering {node.__class__.__name__}')
        super().generic_visit(node)
visitor = MyVisitor()
visitor.visit(tree)
```