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,