CPython bug? Observable wrong assignment order

class C(str):
    def __del__(self):
        print(self)

def f():
    a = C('This should happen first')
    b = C('This should happen afterwards')
    a, b = [], []

f()

The a, b = [], [] is supposed to assign left-to-right, thus also unassigning the previous values left-to-right, so I expect this output that 3.10 does give:

This should happen first
This should happen afterwards

Output in 3.12 (main):

This should happen afterwards
This should happen first

Or is that considered ok? The SWAPPABLE comment mentions “can’t invoke arbitrary code (besides finalizers)”

Indeed you can’t rely on __del__() methods being called immediately, they’re called once the interpreter has realised the object is no longer being referenced. CPython’s reference counting means that usually is as soon as you stop referencing it, but if either object had a reference cycle (try doing a.ref = a first) the GC would have to run to detect it. In PyPy they don’t use reference counts at all, so it’ll always happen at some future point.

What you’re seeing there isn’t assignment order. Here’s a way to actually test assignment order:

class C:
    def __setattr__(self, key, val):
        print("Setting", key)

a = C()
a.spam, a.ham = [], []

This shows that spam is indeed assigned prior to ham. In your example, the assignments are both to fast locals, and the optimizer can see that there’s no reference to either of them until both are assigned, so it knows that it can assign them in either order. The order of object destruction, since it isn’t a language guarantee, doesn’t prevent this.

Note that a perfectly compliant Python implementation might use non-refcounting garbage collection, perhaps leaving garbage around until the function ends, and then disposing of any unreferenced objects all at once. In such an implementation, the order of __del__ calls would be arbitrarily chosen (possibly “whichever is earlier in memory gets disposed of first”), even though the assignments would be done in the correct order.

2 Likes

Alright, thanks.

In contrast, code that stores to a locals mapping (STORE_NAME) has to incur the cost of swapping the stack order for up to 3 assignments (beyond that a temporary tuple gets built). Assigning to a locals mapping can execute arbitrary code, so the assignment order matters. For example:

class Locals:
    def __getitem__(self, name):
        print(f'locals get: {name}')
        raise KeyError

    def __setitem__(self, name, value):
        print(f'locals set: {name} = {value}')

f = g = h = lambda x: x
>>> exec('a, b, c = f(1), g(2), h(3)', globals(), Locals())
locals get: f
locals get: g
locals get: h
locals set: a = 1
locals set: b = 2
locals set: c = 3