Question: Consider a simple data class: from ctypes import c_int32, c_int16 from dataclasses import dataclass @dataclass class MyClass: field1: c_int32 field2: c_int16  According to the docs, if we want to make this dataclass compatible with ctypes, we have to define it like this: import ctypes from ctypes import Structure, c_int32, c_int16, sizeof from dataclasses import dataclass, fields @dataclass class MyClass(ctypes.Structure): _pack_ = 1 _fields_ = [("field1", c_int32),("field2", c_int16)] print(ctypes.sizeof(MyClass))  But unfortunately, this definition deprives us of the convenient features of the dataclass, which are called “dunder” methods. For example, constructor(__init__()) and string representation(__repr__()) become unavailable: inst = MyClass(c_int32(42), c_int16(43)) # will give error Q: What is the most elegant and idiomatic way to make a dataclass compatible with ctypes without losing “dunder” methods? If we ask me, this code seems to work at first glance: @dataclass class MyClass(ctypes.Structure): field1: c_int32 field2: c_int16 _pack_ = 1 MyClass._fields_ = [(field.name, field.type) for field in fields(MyClass)] #_pack_ is skipped  Since I'm a beginner, I'm not sure if this code doesn't lead to some other, non-obvious problems.

Answer: Both ctypes.Structure and dataclass have some similar functionality - but neither was built with the explicit intent of being collaborative with the other - therefore we have to make this bridging code. For one, the dataclass decorator will always attempt to be less disruptive as it can to whatever functionalities the class already have. Since Structure already provides an __init__ method, which works for it, we have to tell dataclass to leave it in place - this can be done just passing the init=False argument to dataclass: A Structure class created this way will work, but won't have a __repr__ - dataclass needs the fields to be annotated in order to know its things. The following decorator would work instead of @dataclass: def sdataclass(cls=None,/, **kwargs): if cls is None: return lambda cls: sdataclass(cls, **kwargs) dataclassdeco = dataclass(init=False, **kwargs) cls.__annotations__ = dict(cls._fields_) return dataclassdeco(cls) @sdataclass class S(ctypes.Structure): _fields_ = [("field1", ctypes.c_uint8),] _pack_ = 1  The small amount of logic inside the decorator is just so that it can pass extra parameters to the original dataclass call- the only important things there are setting the .__annotation__ fields and passing the init=False parameter to the dataclass. HOWEVER, dataclasses come at a later type than structures, when the annotation syntax is in place, and it is more convenient to declare a class fields than with the _fields_ parameter. A converse decorator can just do what you have done in the second part of your question - just passing the init=False decorator. (I am not sure the _pack_ can be set after the class is created - please test the functionality) import ctypes from dataclasses import fields, dataclass def sdataclass(cls=None,/, **kwargs): if cls is None: return lambda cls: sdataclass(cls, **kwargs) # Allow the pack information to be passed # in the decorator: pack = kwargs.pop("pack", True) dataclassdeco = dataclass(init=False, **kwargs) cls = dataclassdeco(cls) cls._fields_ = [(field.name, field.type) for field in fields(cls)] cls._pack_ = pack return cls ... @sdataclass class S(ctypes.Structure): field1: ctypes.c_uint8 field2: ctypes.c_uint16  of course, ctypes structures has some extra functionalities, like allowing the creation of bitfields - this won't suffice as it is - but it should be enough for composing complex structures as it is, without making use of forward declarations.