Search code examples
pythonpropertiesdefaultpython-dataclasses

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


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?


Solution

  • 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...