From dcee516a0e6d2c19b25b4bbc1224ce3fb648f2f0 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 19 Oct 2026 15:20:05 -0400 Subject: [PATCH 1/3] Use PyObject_HashNotImplemented for ctypes data types. --- Lib/test/test_ctypes/test_simplesubclasses.py | 25 ++++++++++++++++++- ...6-10-19-12-00-00.gh-issue-78327.Qk3hzA.rst | 2 ++ Modules/_ctypes/_ctypes.c | 12 +-------- 3 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-19-12-00-00.gh-issue-78327.Qk3hzA.rst 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..e82a2bb821e7796 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__ @@ -3213,7 +3203,7 @@ static PyMethodDef PyCData_methods[] = { static PyType_Slot pycdata_slots[] = { {Py_tp_dealloc, PyCData_dealloc}, - {Py_tp_hash, PyCData_nohash}, + {Py_tp_hash, PyObject_HashNotImplemented}, {Py_tp_doc, PyDoc_STR("XXX to be provided")}, {Py_tp_traverse, PyCData_traverse}, {Py_tp_clear, PyCData_clear}, From 08b6a77b4b04ea46ef7e9c4f4f0116ab46b9a4e0 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 19 Oct 2026 16:01:34 -0400 Subject: [PATCH 2/3] Stupid MSVC ruining my day. --- Modules/_ctypes/_ctypes.c | 57 +++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index e82a2bb821e7796..9099145ef3acdfe 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -3201,25 +3201,39 @@ static PyMethodDef PyCData_methods[] = { { NULL, NULL }, }; -static PyType_Slot pycdata_slots[] = { - {Py_tp_dealloc, PyCData_dealloc}, - {Py_tp_hash, PyObject_HashNotImplemented}, - {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("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}, + }; + 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) @@ -6239,7 +6253,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, From 983cfb42f9615259e85f5d5bcc61b645dd0fc426 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 19 Oct 2026 16:04:06 -0400 Subject: [PATCH 3/3] Add a docstring. I'm already here, so why not? --- Modules/_ctypes/_ctypes.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 9099145ef3acdfe..67e4835ea28bdbd 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -3216,7 +3216,8 @@ PyCData_create_type(PyObject *mod) PyType_Slot slots[] = { {Py_tp_dealloc, PyCData_dealloc}, {Py_tp_hash, PyObject_HashNotImplemented}, - {Py_tp_doc, PyDoc_STR("XXX to be provided")}, + {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},