From 79906b5ad44fc34d0f8d9adcd8e21b04e28c93dd Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 9 Oct 2025 16:37:49 +0200 Subject: [PATCH 1/4] gh-139852: Add PyObject_GetDict() function --- Doc/c-api/object.rst | 26 +++++++++ Doc/whatsnew/3.15.rst | 8 +++ Include/cpython/object.h | 3 +- Lib/test/test_capi/test_object.py | 45 ++++++++++++++++ ...-10-09-16-49-37.gh-issue-139852.76adZW.rst | 2 + ...-10-10-10-33-10.gh-issue-139852.SEDl82.rst | 2 + Modules/_testcapi/object.c | 41 ++++++++++++++ Objects/object.c | 53 +++++++++++++++---- 8 files changed, 169 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/C_API/2025-10-09-16-49-37.gh-issue-139852.76adZW.rst create mode 100644 Misc/NEWS.d/next/C_API/2025-10-10-10-33-10.gh-issue-139852.SEDl82.rst diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 78599e704b1317..9e04fe28ce378d 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -268,6 +268,8 @@ Object Protocol A generic implementation for the getter of a ``__dict__`` descriptor. It creates the dictionary if necessary. + Raise an :exc:`AttributeError` if the object has no ``__dict__``. + This function may also be called to get the :py:attr:`~object.__dict__` of the object *o*. Pass ``NULL`` for *context* when calling it. Since this function may need to allocate memory for the @@ -287,6 +289,27 @@ Object Protocol .. versionadded:: 3.3 +.. c:function:: int PyObject_GetDict(PyObject *obj, PyObject **dict) + + Get a pointer to :py:attr:`~object.__dict__` of the object *obj*. + + * If there is a ``__dict__``, set *\*dict* to a :term:`strong reference` + to the dictionary and return ``1``. + * If there is no ``__dict__``, set *\*dict* to ``NULL`` without setting + an exception and return ``0``. + * On error, set an exception and return ``-1``. + + This function may need to allocate memory for the dictionary, so it may be + more efficient to call :c:func:`PyObject_GetAttr` when accessing an + attribute on the object. + + .. versionadded:: next + + .. seealso:: + :c:func:`PyObject_GenericGetDict` and :c:func:`PyObject_GenericSetDict` + functions. + + .. c:function:: PyObject** _PyObject_GetDictPtr(PyObject *obj) Return a pointer to :py:attr:`~object.__dict__` of the object *obj*. @@ -296,6 +319,9 @@ Object Protocol dictionary, so it may be more efficient to call :c:func:`PyObject_GetAttr` when accessing an attribute on the object. + .. deprecated:: 3.15 + Use :c:func:`PyObject_GetDict` or :c:func:`PyObject_GetAttr` instead. + .. c:function:: PyObject* PyObject_RichCompare(PyObject *o1, PyObject *o2, int opid) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 40286d4fe857e8..709f2f55f933fe 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -852,6 +852,10 @@ New features (Contributed by Victor Stinner in :gh:`129813`.) +* Add :c:type:`PyObject_GetDict` function to get the + :py:attr:`~object.__dict__` of an object. + (Contributed by Victor Stinner in :gh:`139852`.) + * Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array. (Contributed by Victor Stinner in :gh:`111489`.) @@ -915,6 +919,10 @@ Deprecated C APIs since 3.15 and will be removed in 3.17. (Contributed by Nikita Sobolev in :gh:`136355`.) +* Deprecate private :c:func:`_PyObject_GetDictPtr` function: + use public :c:func:`PyObject_GetDict` or :c:func:`PyObject_GetAttr` instead. + (Contributed by Victor Stinner in :gh:`139852`.) + .. Add C API deprecations above alphabetically, not here at the end. diff --git a/Include/cpython/object.h b/Include/cpython/object.h index e2f87524c218b6..f5fd27108dd02c 100644 --- a/Include/cpython/object.h +++ b/Include/cpython/object.h @@ -299,7 +299,8 @@ PyAPI_FUNC(void) _PyObject_Dump(PyObject *); PyAPI_FUNC(PyObject*) _PyObject_GetAttrId(PyObject *, _Py_Identifier *); -PyAPI_FUNC(PyObject **) _PyObject_GetDictPtr(PyObject *); +PyAPI_FUNC(int) PyObject_GetDict(PyObject *obj, PyObject **dict); +Py_DEPRECATED(3.15) PyAPI_FUNC(PyObject **) _PyObject_GetDictPtr(PyObject *); PyAPI_FUNC(void) PyObject_CallFinalizer(PyObject *); PyAPI_FUNC(int) PyObject_CallFinalizerFromDealloc(PyObject *); diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index d4056727d07fbf..e4d41a30ba8992 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -247,5 +247,50 @@ def func(x): func(object()) + def test_object_getdict(self): + # Test PyObject_GetDict() + object_getdict = _testcapi.object_getdict + + class MyClass: + pass + obj = MyClass() + obj.attr = 123 + + dict1 = object_getdict(obj) + dict2 = obj.__dict__ + self.assertIs(dict1, dict2) + + class NoDict: + __slots__ = () + obj = NoDict() + + self.assertEqual(object_getdict(obj), AttributeError) + + # CRASHES object_getdict(NULL) + + def test_object_genericgetdict(self): + # Test PyObject_GenericGetDict() + object_genericgetdict = _testcapi.object_genericgetdict + + class MyClass: + pass + obj = MyClass() + obj.attr = 123 + + dict1 = object_genericgetdict(obj) + dict2 = obj.__dict__ + self.assertIs(dict1, dict2) + + class NoDict: + __slots__ = () + obj = NoDict() + + with self.assertRaisesRegex(AttributeError, + "This object has no __dict__"): + object_genericgetdict(obj) + + # CRASHES object_genericgetdict(NULL) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/C_API/2025-10-09-16-49-37.gh-issue-139852.76adZW.rst b/Misc/NEWS.d/next/C_API/2025-10-09-16-49-37.gh-issue-139852.76adZW.rst new file mode 100644 index 00000000000000..c8d29c143a3e91 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-10-09-16-49-37.gh-issue-139852.76adZW.rst @@ -0,0 +1,2 @@ +Add :c:type:`PyObject_GetDict` function to get the :py:attr:`~object.__dict__` +of an object. Patch by Victor Stinner. diff --git a/Misc/NEWS.d/next/C_API/2025-10-10-10-33-10.gh-issue-139852.SEDl82.rst b/Misc/NEWS.d/next/C_API/2025-10-10-10-33-10.gh-issue-139852.SEDl82.rst new file mode 100644 index 00000000000000..657bf54d73caa1 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-10-10-10-33-10.gh-issue-139852.SEDl82.rst @@ -0,0 +1,2 @@ +Deprecate private :c:func:`_PyObject_GetDictPtr` function: use public +:c:func:`PyObject_GetDict` or :c:func:`PyObject_GetAttr` instead. diff --git a/Modules/_testcapi/object.c b/Modules/_testcapi/object.c index 798ef97c495aeb..6f7c96ff596752 100644 --- a/Modules/_testcapi/object.c +++ b/Modules/_testcapi/object.c @@ -485,6 +485,45 @@ is_uniquely_referenced(PyObject *self, PyObject *op) } +static PyObject * +object_getdict(PyObject *self, PyObject *obj) +{ + NULLABLE(obj); + + PyObject *dict = UNINITIALIZED_PTR; + switch (PyObject_GetDict(obj, &dict)) { + case -1: + assert(dict == NULL); + return NULL; + case 0: + assert(dict == NULL); + return Py_NewRef(PyExc_AttributeError); + case 1: + return dict; + default: + Py_FatalError("PyObject_GetDict() returned invalid code"); + Py_UNREACHABLE(); + } +} + + +static PyObject * +object_genericgetdict(PyObject *self, PyObject *obj) +{ + NULLABLE(obj); + + PyObject *dict = PyObject_GenericGetDict(obj, NULL); + if (dict != NULL) { + return dict; + } + + if (PyErr_Occurred()) { + return NULL; + } + return Py_NewRef(PyExc_AttributeError); +} + + static PyMethodDef test_methods[] = { {"call_pyobject_print", call_pyobject_print, METH_VARARGS}, {"pyobject_print_null", pyobject_print_null, METH_VARARGS}, @@ -511,6 +550,8 @@ static PyMethodDef test_methods[] = { {"test_py_is_funcs", test_py_is_funcs, METH_NOARGS}, {"clear_managed_dict", clear_managed_dict, METH_O, NULL}, {"is_uniquely_referenced", is_uniquely_referenced, METH_O}, + {"object_getdict", object_getdict, METH_O}, + {"object_genericgetdict", object_genericgetdict, METH_O}, {NULL}, }; diff --git a/Objects/object.c b/Objects/object.c index 0540112d7d2acf..6aeaff9563b95d 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1540,6 +1540,28 @@ _PyObject_ComputedDictPointer(PyObject *obj) return (PyObject **) ((char *)obj + dictoffset); } +static int +object_getdictptr(PyObject *obj, PyObject ***dict_ptr) +{ + if ((Py_TYPE(obj)->tp_flags & Py_TPFLAGS_MANAGED_DICT) == 0) { + *dict_ptr = _PyObject_ComputedDictPointer(obj); + return (*dict_ptr != NULL); + } + + PyDictObject *dict = _PyObject_GetManagedDict(obj); + if (dict == NULL && Py_TYPE(obj)->tp_flags & Py_TPFLAGS_INLINE_VALUES) { + dict = _PyObject_MaterializeManagedDict(obj); + if (dict == NULL) { + *dict_ptr = NULL; + return -1; + } + } + *dict_ptr = (PyObject **)&_PyObject_ManagedDictPointer(obj)->dict; + assert(*dict_ptr != NULL); + return 1; +} + + /* Helper to get a pointer to an object's __dict__ slot, if any. * Creates the dict from inline attributes if necessary. * Does not set an exception. @@ -1550,20 +1572,31 @@ _PyObject_ComputedDictPointer(PyObject *obj) PyObject ** _PyObject_GetDictPtr(PyObject *obj) { - if ((Py_TYPE(obj)->tp_flags & Py_TPFLAGS_MANAGED_DICT) == 0) { - return _PyObject_ComputedDictPointer(obj); + PyObject **dict_ptr; + if (object_getdictptr(obj, &dict_ptr) < 0) { + PyErr_Clear(); } - PyDictObject *dict = _PyObject_GetManagedDict(obj); - if (dict == NULL && Py_TYPE(obj)->tp_flags & Py_TPFLAGS_INLINE_VALUES) { - dict = _PyObject_MaterializeManagedDict(obj); - if (dict == NULL) { - PyErr_Clear(); - return NULL; - } + return dict_ptr; +} + + +int +PyObject_GetDict(PyObject *obj, PyObject **dict) +{ + PyObject **dict_ptr; + int res = object_getdictptr(obj, &dict_ptr); + if (res == 1) { + assert(*dict_ptr != NULL); + *dict = Py_NewRef(*dict_ptr); } - return (PyObject **)&_PyObject_ManagedDictPointer(obj)->dict; + else { + assert(dict_ptr == NULL); + *dict = NULL; + } + return res; } + PyObject * PyObject_SelfIter(PyObject *obj) { From 8fca0cdfc30faef78ff4022ff38aa6b77f67993f Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 13 Oct 2025 13:43:05 +0200 Subject: [PATCH 2/4] Recommend PyObject_GetDict() --- Doc/c-api/object.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 9e04fe28ce378d..d0d258f06a9748 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -278,6 +278,10 @@ Object Protocol On failure, returns ``NULL`` with an exception set. + The :c:func:`PyObject_GetDict` function is recommended instead of using this + function, since it does not raise an exception if the object has no + ``__dict__``. + .. versionadded:: 3.3 From ba6ebef7c6fac5574fa160802de937a7e627c70a Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 14 Oct 2025 15:18:02 +0200 Subject: [PATCH 3/4] Complete _PyObject_GetDictPtr() migration guide --- Doc/whatsnew/3.15.rst | 5 +++-- .../C_API/2025-10-10-10-33-10.gh-issue-139852.SEDl82.rst | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 709f2f55f933fe..9c2cf2d3c34e2a 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -919,8 +919,9 @@ Deprecated C APIs since 3.15 and will be removed in 3.17. (Contributed by Nikita Sobolev in :gh:`136355`.) -* Deprecate private :c:func:`_PyObject_GetDictPtr` function: - use public :c:func:`PyObject_GetDict` or :c:func:`PyObject_GetAttr` instead. +* Deprecate the private :c:func:`_PyObject_GetDictPtr` function: + use public :c:func:`PyObject_GetDict`, :c:func:`PyObject_GetAttr`, + :c:func:`PyObject_SetAttr` or :c:func:`PyObject_ClearManagedDict` instead. (Contributed by Victor Stinner in :gh:`139852`.) diff --git a/Misc/NEWS.d/next/C_API/2025-10-10-10-33-10.gh-issue-139852.SEDl82.rst b/Misc/NEWS.d/next/C_API/2025-10-10-10-33-10.gh-issue-139852.SEDl82.rst index 657bf54d73caa1..3f0f673e093bd9 100644 --- a/Misc/NEWS.d/next/C_API/2025-10-10-10-33-10.gh-issue-139852.SEDl82.rst +++ b/Misc/NEWS.d/next/C_API/2025-10-10-10-33-10.gh-issue-139852.SEDl82.rst @@ -1,2 +1,3 @@ -Deprecate private :c:func:`_PyObject_GetDictPtr` function: use public -:c:func:`PyObject_GetDict` or :c:func:`PyObject_GetAttr` instead. +Deprecate the private :c:func:`_PyObject_GetDictPtr` function: use public +:c:func:`PyObject_GetDict`, :c:func:`PyObject_GetAttr`, +:c:func:`PyObject_SetAttr` or :c:func:`PyObject_ClearManagedDict` instead. From e0f2ede5eb4aebd05dd49f1629c75dda89dba32c Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 14 Oct 2025 15:23:25 +0200 Subject: [PATCH 4/4] Update doc --- Doc/c-api/object.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 3c266d87ee6090..9426e9ec714da5 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -324,7 +324,9 @@ Object Protocol when accessing an attribute on the object. .. deprecated:: 3.15 - Use :c:func:`PyObject_GetDict` or :c:func:`PyObject_GetAttr` instead. + Use :c:func:`PyObject_GetDict`, :c:func:`PyObject_GetAttr`, + :c:func:`PyObject_SetAttr` or :c:func:`PyObject_ClearManagedDict` + instead. .. c:function:: PyObject* PyObject_RichCompare(PyObject *o1, PyObject *o2, int opid)