If you have an inheritance there are no two objects, there is only one that is created based on a model that is defined in two parts. You can see more about What is the difference between a class and an object?.
But in this case you are only using composition, so you have one object that contains another. That "contains" is what needs to be explained. Doesn’t mean one object is inside another.
All objects you create with a class exist by themselves and are always allocated independently. The object exists in two parts: a reference that points to the object, and the object itself. It is a type by reference. It would be a pointer, but in Python it’s even a little more complicated than that.
A list is also an object by reference. So self.endereços
, roughly speaking, it only has the pointer to the list itself. The list is allocated to another memory location, it is not inside Cliente
. There inside Cliente
has only the pointer.
Inside this list object you will have pointers to objects. In this case we see that the code places objects of type Endereco
. This object that stores the address is in another memory position, it is not inside the list that has only one pointer to the real object.
There’s a connection from Cliente
with the list called enderecos
that has a connection to an object Endereco
. Connection is different than being all together inside each other.
When an object is deleted, Python’s memory management mechanism, which is primarily a reference count, will do a job to see if it should erase the objects attached to it. If no one else refers to that object it is deleted. But if somewhere else you have a reference to a Endereco
for example, this object will not be erased, it needs to survive not to make unviable this other location.
You can see more details on How Python manages memory during assignment of different types? and What is the difference between Association, Aggregation and Composition in OOP?.