What is the earliest moment when a Python object is eligible for garbage collection and finalization?

Take this program:

import weakref


class Spam:
	def __init__(self):
		weakref.finalize(self, lambda: print("finalized"))
		self.x = 5

	def spam(self):
		print(self.x)


Spam().spam()

Of course CPython will first print ‘5’ and then ‘finalized’, but is it permissible for an alternative implementation to do otherwise?

No, because although it’s not held in any variable, there definitely has to be a temporary storage for in progress computations - the stack or registers. Those hold a strong reference to the Spam object, then when the method accessed a reference to that (which then has a reference to the object). Only after the method has finished being called could the reference be dropped.

Failing to have strong references to temporaries would make an implementation exceptionally broken. Even something as simple as x = [] could then break, if the list was garbage collected before being assigned. If it was a generator or async function, then that object would need to somehow keep references also if it gets paused.

It is held in a variable, self. But that in itself may be insufficient. After the attribute lookup, the variable is not needed any more and might as well be discarded. I happen to know this is exactly the situation in Java. And sure, this makes writing native bindings for the JVM more tricky, but it seems it hasn’t collapsed under the weight of its own complexity just yet.

Is the Spam being held alive for the duration of Spam.spam guaranteed by something more tangible than ‘come on, that would have been absurd’? Is an explicit GC fence necessary in Python (and how to write it?), or is merely being held in a local variable like self sufficient to keep an object from being collected and finalized? Where is this documented?

Accessing the attribute Spam().spam creates and binds a method that references the instance as its __self__ attribute. In the call Spam().spam(), the self argument borrows the __self__ reference. After the call returns, the outer frame’s reference to the bound method Spam().spam is discarded. Since the method is no longer referenced, it can be finalized and deallocated at any time. The method object has the only remaining reference to the Spam() instance, which can be finalized and deallocated any time after the method is deallocated.

CPython uses a reference counting system that finalizes an object as soon as the reference count decrements to 0, with the help of a garbage collector that breaks reference cycles. Other implementations may use more sophisticated garbage collectors. It’s thus not a good idea to rely on __del__() for explicit resource management, as opposed to a with statement that calls an object’s __enter__() and __exit__() methods.

After the lookup the reference to the instance is discarded, but accessing a method on an instance actually creates a “bound method” object, which has a reference to the original function (__func__) and the bound object (__self__). Then when that is called self gets prepended. Once the call is complete, the reference to the method is discarded, which then drops the reference to the instance, which can then be finalized. As an optimisation direct method calls don’t actually create this method object, but that’s only done where it doesn’t change behaviour.

Unlike Java Python doesn’t have a detailed memory model, specifically how it behaves isn’t an implementation requirement including whether a GC exists. You can turn it off even via gc.disable(), CPython mainly relies on reference counting actually. The GC is only required to detect circular references.

Sorry, it is not clear to me whether you are referring to the Spam object itself (a class – remember that in Python classes are objects too) or the temperary Spam instance.

My guess is that you are referring to the instance created with Spam() rather than Spam itself. Is that correct?

I meant the instance yeah, though the stack/register is going to first hold a reference to the class to call it, then the instance, then the bound method.