https://github.com/python/cpython/commit/3f09229cb7f92106cd9afa7091eaf4443e8aaef6
commit: 3f09229cb7f92106cd9afa7091eaf4443e8aaef6
branch: main
author: Peter Bierma <[email protected]>
committer: ZeroIntensity <[email protected]>
date: 2026-10-19T18:12:18-04:00
summary:

gh-78327: Use `PyObject_HashNotImplemented` for `ctypes` data types (GH-159736)

files:
A Misc/NEWS.d/next/Library/2026-10-19-12-00-00.gh-issue-78327.Qk3hzA.rst
M Lib/test/test_ctypes/test_simplesubclasses.py
M Modules/_ctypes/_ctypes.c

diff --git a/Lib/test/test_ctypes/test_simplesubclasses.py 
b/Lib/test/test_ctypes/test_simplesubclasses.py
index 4e4bef3690f66a1..5f3ffb84ea4c34a 100644
--- a/Lib/test/test_ctypes/test_simplesubclasses.py
+++ b/Lib/test/test_ctypes/test_simplesubclasses.py
@@ -1,5 +1,6 @@
 import unittest
-from ctypes import Structure, CFUNCTYPE, c_int, _SimpleCData
+from collections.abc import Hashable
+from ctypes import Structure, CFUNCTYPE, c_int, _SimpleCData, POINTER
 from ._support import (_CData, PyCSimpleType, 
Py_TPFLAGS_DISALLOW_INSTANTIATION,
                        Py_TPFLAGS_IMMUTABLETYPE)
 
@@ -26,6 +27,28 @@ def test_type_flags(self):
                 self.assertTrue(_SimpleCData.__flags__ & 
Py_TPFLAGS_IMMUTABLETYPE)
                 self.assertFalse(_SimpleCData.__flags__ & 
Py_TPFLAGS_DISALLOW_INSTANTIATION)
 
+    def test_unhashable(self):
+        # gh-78327: CData objects are mutable and must not be hashable.
+        class S(Structure):
+            _fields_ = [("a", c_int)]
+
+        for cls in c_int, S, c_int * 2, POINTER(c_int), CFUNCTYPE(None):
+            with self.subTest(cls=cls):
+                self.assertIsNone(cls.__hash__)
+                obj = cls()
+                self.assertNotIsInstance(obj, Hashable)
+                with self.assertRaisesRegex(TypeError,
+                                            f"unhashable type: 
'{cls.__name__}'"):
+                    hash(obj)
+
+        # Subclasses can still opt in to hashing.
+        class H(S):
+            def __hash__(self):
+                return 42
+
+        self.assertEqual(hash(H()), 42)
+        self.assertIsInstance(H(), Hashable)
+
     def test_metaclass_details(self):
         # Abstract classes (whose metaclass __init__ was not called) can't be
         # instantiated directly
diff --git 
a/Misc/NEWS.d/next/Library/2026-10-19-12-00-00.gh-issue-78327.Qk3hzA.rst 
b/Misc/NEWS.d/next/Library/2026-10-19-12-00-00.gh-issue-78327.Qk3hzA.rst
new file mode 100644
index 000000000000000..b0d4107bcadf931
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-19-12-00-00.gh-issue-78327.Qk3hzA.rst
@@ -0,0 +1,2 @@
+:mod:`ctypes` data types now set :meth:`~object.__hash__` to ``None``, so
+:class:`collections.abc.Hashable` checks report them correctly.
diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c
index 43f416c141fd8ad..67e4835ea28bdbd 100644
--- a/Modules/_ctypes/_ctypes.c
+++ b/Modules/_ctypes/_ctypes.c
@@ -3105,16 +3105,6 @@ PyCData_NewGetBuffer(PyObject *myself, Py_buffer *view, 
int flags)
     return 0;
 }
 
-/*
- * CData objects are mutable, so they cannot be hashable!
- */
-static Py_hash_t
-PyCData_nohash(PyObject *self)
-{
-    PyErr_SetString(PyExc_TypeError, "unhashable type");
-    return -1;
-}
-
 /*[clinic input]
 @critical_section
 _ctypes.PyCData.__reduce__
@@ -3211,25 +3201,40 @@ static PyMethodDef PyCData_methods[] = {
     { NULL, NULL },
 };
 
-static PyType_Slot pycdata_slots[] = {
-    {Py_tp_dealloc, PyCData_dealloc},
-    {Py_tp_hash, PyCData_nohash},
-    {Py_tp_doc, PyDoc_STR("XXX to be provided")},
-    {Py_tp_traverse, PyCData_traverse},
-    {Py_tp_clear, PyCData_clear},
-    {Py_tp_methods, PyCData_methods},
-    {Py_tp_members, PyCData_members},
-    {Py_bf_getbuffer, PyCData_NewGetBuffer},
-    {0, NULL},
-};
-
-static PyType_Spec pycdata_spec = {
-    .name = "_ctypes._CData",
-    .basicsize = sizeof(CDataObject),
-    .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
-              Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_DISALLOW_INSTANTIATION),
-    .slots = pycdata_slots,
-};
+/*
+ * CData objects are mutable, so they cannot be hashable. Setting tp_hash to
+ * PyObject_HashNotImplemented() makes type_ready() set __hash__ to None.
+ *
+ * Unfortunately, on Windows, PyObject_HashNotImplemented is imported from
+ * the Python DLL, and MSVC resolves the address of an imported function in
+ * a static initializer to a local thunk that can't be recognized. So, we
+ * initialize the slots at runtime to load the real address from the import 
table.
+ */
+static PyObject *
+PyCData_create_type(PyObject *mod)
+{
+    PyType_Slot slots[] = {
+        {Py_tp_dealloc, PyCData_dealloc},
+        {Py_tp_hash, PyObject_HashNotImplemented},
+        {Py_tp_doc, PyDoc_STR("The common base class of all ctypes "
+                              "data types.")},
+        {Py_tp_traverse, PyCData_traverse},
+        {Py_tp_clear, PyCData_clear},
+        {Py_tp_methods, PyCData_methods},
+        {Py_tp_members, PyCData_members},
+        {Py_bf_getbuffer, PyCData_NewGetBuffer},
+        {0, NULL},
+    };
+    PyType_Spec spec = {
+        .name = "_ctypes._CData",
+        .basicsize = sizeof(CDataObject),
+        .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
+                  Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_IMMUTABLETYPE |
+                  Py_TPFLAGS_DISALLOW_INSTANTIATION),
+        .slots = slots,
+    };
+    return PyType_FromMetaclass(NULL, mod, &spec, NULL);
+}
 
 static int
 PyCData_MallocBuffer(CDataObject *obj, StgInfo *info)
@@ -6249,7 +6254,10 @@ _ctypes_add_types(PyObject *mod)
     */
     CREATE_TYPE(st->PyCArg_Type, &carg_spec, NULL, NULL);
     CREATE_TYPE(st->PyCThunk_Type, &cthunk_spec, NULL, NULL);
-    CREATE_TYPE(st->PyCData_Type, &pycdata_spec, NULL, NULL);
+    st->PyCData_Type = (PyTypeObject *)PyCData_create_type(mod);
+    if (st->PyCData_Type == NULL) {
+        return -1;
+    }
 
     // Common Metaclass
     CREATE_TYPE(st->PyCType_Type, &pyctype_type_spec,

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to