Skip to content

Fix memory leak: clear managed dict in pybind11_object_dealloc on Python 3.13+ #5999

Open
yamedvedya wants to merge 1 commit intopybind:masterfrom
yamedvedya:fix-python314-dynamic-atribute-memroy-leak
Open

Fix memory leak: clear managed dict in pybind11_object_dealloc on Python 3.13+ #5999
yamedvedya wants to merge 1 commit intopybind:masterfrom
yamedvedya:fix-python314-dynamic-atribute-memroy-leak

Conversation

@yamedvedya
Copy link

@yamedvedya yamedvedya commented Feb 27, 2026

Closes #5998

Problem

On Python 3.14, objects stored in the __dict__ of py::dynamic_attr() instances are permanently leaked — their refcounts are abandoned when the pybind11 object is freed, so destructors (including capsule destructors) never run.

Root cause: pybind11_object_dealloc() calls type->tp_free(self) without first calling PyObject_ClearManagedDict(self). On Python ≤ 3.13, PyObject_GC_Del (what tp_free resolves to for GC-tracked objects) cleared the managed dict as a side effect. On Python 3.14 this implicit clearing was removed, requiring an explicit call in tp_dealloc.

The existing pybind11_clear() (which correctly calls PyObject_ClearManagedDict) is only invoked by the cyclic GC via tp_clear — it is never called during normal reference-count-driven deallocation.

Fix

// pybind11/detail/class.h — pybind11_object_dealloc()

  #if PY_VERSION_HEX >= 0x030D0000
      if (PyType_HasFeature(type, Py_TPFLAGS_MANAGED_DICT)) {
          PyObject_ClearManagedDict(self);
      }
  #endif

PyObject_ClearManagedDict is idempotent, so calling it before tp_free is safe on all Python 3.13+ versions.

Test

Added test_dynamic_attr_dealloc_frees_dict_contents to test_methods_and_attributes:

  • Creates a DynamicClass (py::dynamic_attr()) instance
  • Stores a py::capsule in its __dict__; the capsule destructor sets a global flag
  • Asserts the flag is set after del instance + gc_collect()

Impact

Any py::dynamic_attr() class that stores Python objects in instance __dict__ from C++ (via obj.attr("x") = value) is affected. A prominent real-world case is https://gitlab.com/tango-controls/pytango, where
Tango::DeviceAttribute uses py::dynamic_attr() to store zero-copy capsule-backed numpy arrays — these arrays leaked completely under Python 3.14 (https://gitlab.com/tango-controls/pytango/-/issues/744).

On Python 3.14, PyObject_GC_Del (tp_free) no longer implicitly clears
the managed dict of objects with Py_TPFLAGS_MANAGED_DICT. Without an
explicit PyObject_ClearManagedDict() call before tp_free(), objects
stored in the __dict__ of py::dynamic_attr() instances have their
refcounts permanently abandoned, causing memory leaks — capsule
destructors for numpy arrays (and other objects) never run.

Adds a regression test: stores a py::capsule in the __dict__ of a
DynamicClass instance and asserts the capsule destructor is called
when the instance is deleted.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: py::dynamic_attr() objects leak their __dict__ contents on Python 3.14 — capsule destructors never called

1 participant