I need to be able to seamlessly interact with half-precision floating-point values in a ctypes structure. I have a working solution, but I'm dissatisfied with it: import ctypes import struct packed = struct.pack('<Ife', 4, 2.3, 1.2) print('Packed:', packed.hex()) class c_half(ctypes.c_ubyte*2): @property def value(self) -> float: result, = struct.unpack('e', self) return result class Triple(ctypes.LittleEndianStructure): _pack_ = 1 _fields_ = ( ('index', ctypes.c_uint32), ('x', ctypes.c_float), ('y', c_half), ) unpacked = Triple.from_buffer_copy(packed) print(unpacked.y.value)  Packed: 0400000033331340cd3c 1.2001953125  I am dissatisfied because, unlike with c_float, c_uint32 etc., there is no automatic coercion of the buffer data to the Python primitive (float and int respectively for those examples); I would expect float in this half-precision case. Reading into the CPython source, the built-in types are subclasses of _SimpleCData: static PyType_Spec pycsimple_spec = { .name = "_ctypes._SimpleCData", .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_IMMUTABLETYPE), .slots = pycsimple_slots, };  and only declare a _type_, for instance class c_float(_SimpleCData): _type_ = "f"  However, attempting the naive class c_half(ctypes._SimpleCData): _type_ = 'e'  results in AttributeError: class must define a '_type_' attribute which must be a single character string containing one of 'cbBhHiIlLdfuzZqQPXOv?g'.  as defined by SIMPLE_TYPE_CHARS: static const char SIMPLE_TYPE_CHARS[] = "cbBhHiIlLdfuzZqQPXOv?g"; // ... if (!strchr(SIMPLE_TYPE_CHARS, *proto_str)) { PyErr_Format(PyExc_AttributeError, "class must define a '_type_' attribute which must be\n" "a single character string containing one of '%s'.", SIMPLE_TYPE_CHARS); goto error; }  The end goal is to have a c_half type that I can use with the exact same API as the other built-in ctypes.c_ classes, ideally without myself writing a C module. I think I need to mimic much of the behaviour seen in the neighbourhood of PyCSimpleType_init but that code is difficult for me to follow.
Using descriptors gets close to what you want. Declare the ctypes fields with underscores and add the descriptors as class variables. If you are not familiar with descriptors, read the guide in the link above. import ctypes as ct import struct # Descriptor implementation class Half: def __set_name__(self, owner, name): self.field = f'_{name}' # name of ctypes field def __get__(self, obj, objtype=None): # Translate ctypes c_half field to float data = getattr(obj, self.field) return struct.unpack('e', data)[0] def __set__(self, obj, value): # Translate float to ctypes c_half field setattr(obj, self.field, c_half(*struct.pack('e', value))) # two-byte field with display overrides class c_half(ct.c_ubyte*2): def __repr__(self): return f'c_half({self})' def __str__(self): return str(struct.unpack('e', bytes(self))[0]) class Quad(ct.Structure): y = Half() # Declare descriptors z = Half() # Descriptor name must match _name of ctypes field _pack_ = 1 _fields_ = (('index', ct.c_uint32), ('x', ct.c_float), ('_y', c_half), # ctypes fields ('_z', c_half)) # Only needed if you want Quad(1,2,3,4) construction. # Without it, Quad() initializes all fields to zero # and must set them manually. You can do Quad(1,2) # to set the non-c_half fields (index and x). def __init__(self, index=0, x=0, y=0, z=0): self.index = index self.x = x self.y = y # needed to call descriptor __set__ self.z = z # needed to call descriptor __set__ def __repr__(self): return f'Quad(index={self.index}, x={self.x}, y={self.y}, z={self.z})' # Examples t = Quad(4, 1.2, 2.3, 3.4) print(t) print(f'{t.y=} {repr(t._y)}, {t.z=} {repr(t._z)}') t.y, t.z = 8.8, 9.9 print(f'{t.y=} {repr(t._y)}, {t.z=} {repr(t._z)}')  Output: Quad(index=4, x=1.2000000476837158, y=2.30078125, z=3.400390625) t.y=2.30078125 c_half(2.30078125), t.z=3.400390625 c_half(3.400390625) t.y=8.796875 c_half(8.796875), t.z=9.8984375 c_half(9.8984375) 