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]