Default __post_init__ Implementation in Dataclasses

I’ve encountered an issue with the dataclasses module that I believe could be improved. When using inheritance with dataclasses, if a parent class does not define a __post_init__ method, calling super().__post_init__() in the child class will raise an AttributeError, which is in contrast to super().__init__()

Here’s a minimal example to illustrate the issue:

from dataclasses import dataclass, field
 
@dataclass
class Parent:
    greetP: str = field(default="Hello from Parent")
    # No __post_init__ method explicitly defined

@dataclass
class Child(Parent):
    greetC: str = field(default="Hello from Child")
    
    def __post_init__(self):
        # EDIT: this check using hasattr was suggested in answers.
        # This is very ugly, but AttributeError occurs otherwise
        if hasattr(super(), "__post_init__"):
            super().__post_init__()  
        if self.greetP == self.greetC:
            self.greetP = "Hello from Parent"

    def greet(self) -> str:
        return f"{self.greetC}, {self.greetP}"

I suggest that the dataclasses module should include a default no-op __post_init__ method in the base class. This way, calling super().__post_init__() will always be safe.

If this is a design choice (feature, not a bug), I would be grateful if someone could point out to me the benefit and/or necessity of dataclasses __post_init__ to work this way.

3 Likes

This was previously discussed somewhat extensively in dataclasses should define an empty __post_init__ · Issue #90913 · python/cpython · GitHub

5 Likes

thanks for the reference. I searched for this topic in the search function of this forum, as well as online before asking.

if hasattr(super(), "__post_init__"):
            super().__post_init__()

… as a workaround, from the reference might be a good take-home message for those finding this question in this forum.

Note: this is very ugly, and I have oppinions on this… but likely nothing that would change the conclusions taken on the reference thread given

2 Likes

Hundred percent agree with you here. Unfortunately attrs was not designed to be compatible with Python’s cooperative inheritance, and the dataclass module inherited this design.

One way to make the workaround slightly DRYer would be to use getattr with a default of None instead so that you can store the result of the MRO-based method lookup in a variable and then call it if it isn’t None:

if super_post_init := getattr(super(), "__post_init__", None):
    super_post_init()

You can also consider ignoring AttributeError:

from contextlib import suppress
...
with suppress(AttributeError):
    super().__post_init__()

This will suppress exceptions in the super call itself, which can cause inscrutable bugs. It’s probably better not to use this.

The := version is nice though.

3 Likes

Ah yes very good point. A workaround would be to separate the call from the attribute lookup with an uglier try block:

try:
    super_post_init = super().__post_init__
except AttributeError:
    pass
else:
    super_post_init()

But yeah this then makes the getattr solution look prettier.

If all of the dataclasses in your project inherit only from dataclasses in your own project (which is usually the case), you can simply make sure that they all inherit from the same base class that defines an empty __post_init__ method:

class DataClassBase:
    def __post_init__(self):
        pass

@dataclass
class Parent(DataClassBase):
    greetP: str = field(default="Hello from Parent")
    # No __post_init__ method explicitly defined

@dataclass
class Child(Parent):
    greetC: str = field(default="Hello from Child")
    def __post_init__(self):
        super().__post_init__()  
2 Likes

It might be even DRYer to use
getattr(super(), "__post_init__", lambda:())()
? No if needed if you’re already using getattr with a default value.

Personally I feel like you should reconsider the design of your program if this is something you really need to use. super().__init__() is cursed already, and this is two or three steps beyond that.

1 Like