'Python dataclasses inheritance and default values

Given the following hierarchy of python dataclasses:

@dataclass
class A:
    a: str
    aa: str


@dataclass
class B(A):
    b: str


@dataclass
class C(A):
    c: str


@dataclass
class D(B, C):
    d: str

Is there any general method that allows you to insert an attribute with a default value in this inheritance hierarchy? For instance, the following produces a Non-default argument(s) follows default argument(s) defined in 'B' error for class D.

@dataclass
class A:
    a: str
    aa: str


@dataclass
class B(A):
    b: str = 'b'  # <-- default value


@dataclass
class C(A):
    c: str


@dataclass
class D(B, C):
    d: str

I suppose one solution could be to give all attributes in the hierarchy a default value of None, and add a __post_init__() validation which raises some exception if any attributes are None, but this doesn't seem right and you would need it in every dataclass in the hierarchy.

Class inheritance in Python 3.7 dataclasses proposes a solution where you create two separate branches of the inheritance hierarchy; one for default values and one without. The drawback here is that the hierarchy quickly becomes mangled with a lot of classes, but if there are no better options, I suppose this is as good as it gets.

Edit

I've come up with a meta_dataclass which does some hackery to at least solve my problem. I'm sure it breaks all kinds of rules, but perhaps someone can improve upon it.

It allows a hierarchy of meta_dataclasses where any attribute in the hierarchy can have a default value. It is motivated by the solution presented in Class inheritance in Python 3.7 dataclasses. It effectively creates two inheritance branches, one with default values and one without, but the creation of the extra classes required to achieve this is encapsulated in the mete_dataclass decorator. A current drawback is that a meta_dataclass can only inherit from other meta_dataclasses.

Full code:

from dataclasses import make_dataclass, MISSING, field


class Dataclass:
    pass


class Declarations:
    pass


class Definitions:
    pass


def meta_dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False):
    def wrap(cls):
        declaration_bases = []
        definition_bases = []

        for base in cls.__bases__:
            if issubclass(base, Dataclass):
                declaration_bases += [c for c in base.__bases__ if issubclass(c, Declarations)]
                definition_bases += [c for c in base.__bases__ if issubclass(c, Definitions)]
            elif len(cls.__bases__) == 1 and base != object:
                raise ValueError(f'meta_dataclasses can only inherit from other meta_dataclasses. '
                                 f'{cls} inherits from {base}')

        declaration_bases.append(Declarations)
        definition_bases.append(Definitions)

        fields = []
        if hasattr(cls, '__annotations__'):
            for field_name, field_type in cls.__annotations__.items():
                f = field(default=cls.__dict__[field_name]) if field_name in cls.__dict__ else field()
                fields.append((field_name, field_type, f))

        declarations = make_dataclass(cls_name=f'{cls.__name__}_Declarations',
                                      bases=tuple(declaration_bases),
                                      fields=[f for f in fields if isinstance(f[2].default, type(MISSING))],
                                      init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen)

        definitions = make_dataclass(cls_name=f'{cls.__name__}_Definitions',
                                     bases=tuple(definition_bases),
                                     fields=[f for f in fields if not isinstance(f[2].default, type(MISSING))],
                                     init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen)

        cls_wrapper = make_dataclass(cls_name=cls.__name__, fields=[], bases=(Dataclass, definitions, declarations),
                                     namespace=cls.__dict__, init=init, repr=repr, eq=eq, order=order,
                                     unsafe_hash=unsafe_hash, frozen=frozen)

        return cls_wrapper

    if cls is None:
        return wrap
    else:
        return wrap(cls)

Example:

@meta_dataclass
class A:
    a: str
    aa: str

@meta_dataclass
class B(A):
    b: str = 'b'

@meta_dataclass
class C(A):
    c: str

@meta_dataclass
class D(B, C):
    d: str


>>> help(D)
Help on class D in module types:

class D(__main__.Dataclass, D_Definitions, D_Declarations)
 |  D(a: str, aa: str, c: str, d: str, b: str = 'b') -> None
 |  
 |  D(a: str, aa: str, c: str, d: str, b: str = 'b')
 |  
 |  Method resolution order:
 |      D
 |      __main__.Dataclass
 |      D_Definitions
 |      B_Definitions
 |      C_Definitions
 |      A_Definitions
 |      __main__.Definitions
 |      D_Declarations
 |      B_Declarations
 |      C_Declarations
 |      A_Declarations
 |      __main__.Declarations
 |      builtins.object

Illustration:

The below image illustrates what the @meta_dataclass decorator does to a single class.

Illustration of what @meta_dataclass does



Solution 1:[1]

This is a well-known issue for data classes, there are several workarounds but this is solved every elegantly in Python 3.10 in the PR that solved the issue 43532.

It would work the following way:

from dataclasses import dataclass

@dataclass(kw_only=true)
class Base:
  type: str
  counter: int = 0

@dataclass(kw_only=true)
class Foo(Base):
  id: int

This is deeply explained at: https://medium.com/@aniscampos/python-dataclass-inheritance-finally-686eaf60fbb5

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Rafael Aguilar