How to add a property to a child dataclass before a default property?

Question:

Let’s suppose that I have a dataclass A. The dataclass A contains some property with a default value.

Let’s suppose that I want to extend that dataclass with a dataclass B. I need to add some non-default property to the dataclass B.

However, if I do that, I will get an error: "Non-default property follows a default property in class B". That is because dataclass generates firstly the properties from the class A and then from the class B. So the non-default property declared in the class B follows the default property from the class A (in the auto generated init function for example).

Example:

from dataclasses import dataclass


@dataclass
class A:
    a: int
    b: int = 5


@dataclass
class B(A):
    c: int

The code above will give an error.

How can I therefore add a non-default property to the class B? What are the possible ways to do that without throwing an error?

Asked By: Damian

||

Answers:

You have a first problem that doing that is not possible in Python as a language at all: when creating a function, non-default parameters always have to precede parameters that have a default. It turns out it makes sense.

If it were possible to create a class like this, the signature to creating "B" would necessarily have to be keyword only – as it would not be possible to assign a value to ".c" without giving a value to ".b" – at which point its default makes no sense. But still, the language would allow one to call B(a=10, c=20) and then b could use its default of 5.

So, the most sensible thing to do seems to be to add a default value to your "non-default" attributes: but use a sentinel value, and then detect in the post_init stage if an initial value was not given to these "pseudo-defaulted" fields and raise an error.

It would be possible to wrap the dataclass decorator itself in another decorator that could do the introspection, create the post-init code, and do all of it alone: but that is a lot of work to let things less explicit (still, it could be valid if you are using this pattern a lot).

Otherwise, just add these steps manually:

from dataclasses import dataclass

FALSEDEFAULT = object()

@dataclass
class A:
   a: int
   b: int = 5

@dataclass
class B:
   c: int = FALSEDEFAULT   # one might have problem with type-checkers like mypy
   def __post_init__(self):
       if self.c is FALSEDEFAULT:
            raise TypeError("An argument for "c" has to be passed when creating  B instances")

An intermediary automation step – more useful than this if you are reusing it, and far simpler than wrappign dataclass itself might be to have a base-class with a __post_init__ that will check the values of all fields and raise on any field that contains the "PSEUDODEFAULT" sentinel –

class OutOfOrderBase:
    def __post_init__(self):
         for name, field in self.__dataclass_fields__.items():
             if getattr(self, name, None) is PSEUDODEFAULT: raise...
Answered By: jsbueno