Search code examples
pythonqtpyqtpyqt5ownership

Why is the child QObject still accessible when the parent is deleted?


Trying to learn more about PyQt I wrote a small script that:

  • Creates an QObject a
  • Creates an QObject b with a as its parent
  • Deletes a

At this point I expect the object to which the name b is point to have been deleted as well. The documentation of Qt (not PyQt!) says:

The parent takes ownership of the object; i.e., it will automatically delete its children in its destructor.

But b still points to an existing object.
I also tried an explicit garbage collection without any change.
Trying to access a via b's parent() method fails though, as expected.

Why is the QObject referenced by b not deleted when a, which "owns" b, is deleted?

I have added the print outputs as comments below:

import gc

from PyQt5.QtCore import QObject


def tracked_qobjects():
    return [id(o) for o in gc.get_objects() if isinstance(o, QObject)]


def children(qobject: QObject):
    return [id(c) for c in qobject.findChildren(QObject)]


a = QObject()
b = QObject(parent=a)
print(f"QObjects tracked by gc: {tracked_qobjects()}")
# QObjects tracked by gc: [140325587978704, 140325587978848]
print(f"Children of a: {children(a)}")
# Children of a: [140325587978848]

del a
print(f"QObjects tracked by gc: {tracked_qobjects()}")
# QObjects tracked by gc: [140325587978848]

gc.collect()  # not guaranteed to clean up but should not hurt
print(f"QObjects tracked by gc: {tracked_qobjects()}")
# QObjects tracked by gc: [140325587978848]

# Since https://doc.qt.io/qt-5/qobject.html#details says:
# "The parent takes ownership of the object; i.e., it will automatically delete
# its children in its destructor."
# I expect that b now points to a non-existent object.
# But no, this still works! Maybe because we are in PyQt5 and
# not a C++ application?
print(id(b))
# 140325587978848
print(b)
# <PyQt5.QtCore.QObject object at 0x7fa018d30a60>

# The parent is truly gone though and trying to access it from its child raises the "wanted" RuntimeError
print(b.parent())
# RuntimeError: wrapped C/C++ object of type QObject has been deleted

Solution

  • This was a red herring. Python can still provide the memory address (id) that the name b is point to and the type of the object expected to be there, but the actual object has been destroyed:

    from PyQt5.QtCore import QObject
    
    
    def children(qobject: QObject):
        return [id(c) for c in qobject.findChildren(QObject)]
    
    
    def on_destroyed():
        print("x_x")
    
    
    a = QObject()
    b = QObject(parent=a)
    b.destroyed.connect(on_destroyed)
    print(f"Children of a: {children(a)}")
    
    print("Deleting a...")
    del a
    print("a has been deleted.")
    
    # Since https://doc.qt.io/qt-5/qobject.html#details says:
    # "The parent takes ownership of the object; i.e., it will automatically delete its children in its destructor."
    # b now points to a non-existent object.
    
    # Python still knows its memory address and type:
    print(id(b))
    print(b)
    
    # But trying to do anything with it will fail with the RuntimeError:
    print(b.objectName())
    

    results in

    Children of a: [140051377982048]
    Deleting a...
    x_x
    a has been deleted.
    140051377982048
    <PyQt5.QtCore.QObject object at 0x7f6040a28a60>
    Traceback (most recent call last):
      File "/.../parental_issues.py", line 30, in <module>
        print(b.objectName())  # RuntimeError: wrapped C/C++ object of type QObject has been deleted
              ^^^^^^^^^^^^^^
    RuntimeError: wrapped C/C++ object of type QObject has been deleted
    

    The error message is about b. Its (wrapped) C++ object has been deleted so the Python object is errorneous.