Master Python dataclass Inheritance: 2 Strategies To Avoid any Pitfall

While dataclass makes it easy to define and manage data models, things can get tricky when inheritance comes into play.

One common pain point? Running into cryptic errors like TypeError: non-default argument '...' follows default argument.

If you’ve ever struggled to understand why this happens or how to avoid it, then don’t worry this blog will clear that out.

This error often pops up when mixing required and default fields in child classes, disrupting your flow and leaving you searching for answers.

This blog focuses on demystifying dataclass inheritance and tackling the TypeError.

We’ll explore why this error occurs, how to fix it, and strategies to structure your dataclass hierarchy cleanly and efficiently.

By the end, you’ll have the clear idea of how to work with dataclass ‘s when inheritance is involved!

Simple dataclass example

from dataclasses import dataclass

@dataclass
class User:
    username: str
    email: str
    is_active: bool = True

    def deactivate_account(self):
        """Deactivates the user's account."""
        self.is_active = False
        print(f"{self.username}'s account has been deactivated.")

    def reactivate_account(self):
        """Reactivates the user's account."""
        self.is_active = True
        print(f"{self.username}'s account has been reactivated.")
        

user = User("Arup", "arup@thestartupcoder.com")
user.deactivate_account() 
print(user)

Output:
Arup's account has been deactivated.
User(username='Arup', email='arup@thestartupcoder.com', is_active=False)

Let’s understand the code now. dataclass in Python helps you automatically adding some special methods like __init__() and __repr__().

The above code will add a __init__ which looks something like this –

def __init__(self, username: str, email: str, is_active: bool = True):
    self.username = username
    self.email = email
    self.is_active = is_active

Now, we have some idea about how dataclass is useful in Python. You can pass some arguments to the dataclass decorator like init=True, repr=True, eq=True, and kw_only=True etc.

For more about the arguments that could be passed to dataclass, you can read here.

Inheritance with dataclass

Now, let’s dive into the main topic of this blog… Inheritance with dataclass.

To understand the problem, let’s first see how inheritance with dataclass will work –

from dataclasses import dataclass

@dataclass
class User:
    username: str
    email: str
    is_active: bool = True

    def deactivate_account(self):
        """Deactivates the user's account."""
        self.is_active = False
        print(f"{self.username}'s account has been deactivated.")

    def reactivate_account(self):
        """Reactivates the user's account."""
        self.is_active = True
        print(f"{self.username}'s account has been reactivated.")
        
@dataclass
class Admin(User):
    admin_level: int = 1

    def promote_user(self, user: User):
        """Promotes a user by reactivating their account."""
        if self.admin_level == 1:
            user.reactivate_account()
            print(f"Admin {self.username} has promoted {user.username}.")
        else:
            print(f"You do not have admin access anymore.")

    def revoke_privileges(self):
        """Revokes admin privileges."""
        self.admin_level = 0
        print(f"{self.username} is no longer an admin.")
        
admin = Admin("Arup", "arup@thestartupcoder.com")

admin.promote_user(user)

print(user)
print(admin)

Output:
Arup's account has been reactivated.
Admin Arup has promoted Arup.
User(username='Arup', email='arup@thestartupcoder.com', is_active=True)
Admin(username='Arup', email='arup@thestartupcoder.com', is_active=True, admin_level=1)

In this working code, you can see that the overall syntax is same like how you perform inheritance in Python classes.

How dataclass is working here?

dataclass is building the `__init__` from base to children, so here in this example, the members of `admin` are in the following sequence –

[username, email, is_active, admin_level]

Where does it go wrong?

dataclass inheritance can go wrong in the following scenario –

@dataclass
class User:
    username: str
    email: str
    is_active: bool = True

    def deactivate_account(self):
        """Deactivates the user's account."""
        self.is_active = False
        print(f"{self.username}'s account has been deactivated.")

    def reactivate_account(self):
        """Reactivates the user's account."""
        self.is_active = True
        print(f"{self.username}'s account has been reactivated.")

@dataclass
class Admin(User):
    admin_id: int  # No default value
    admin_level: int = 1

    def promote_user(self, user: User):
        """Promotes a user by reactivating their account."""
        if self.admin_level == 1:
            user.reactivate_account()
            print(f"Admin {self.username} has promoted {user.username}.")
        else:
            print(f"You do not have admin access anymore.")

    def revoke_privileges(self):
        """Revokes admin privileges."""
        self.admin_level = 0
        print(f"{self.username} is no longer an admin.")
        
Error:
    ...
    raise TypeError(f'non-default argument {f.name!r} '
TypeError: non-default argument 'admin_id' follows default argument

In this example, the sequence of members in any object of `Admin` would be –

[username, email, is_active, admin_id, admin_level]

Can you see the problem?

YES! The default argument which in Python is also a keyword argument comes before a non-default argument which can be considered as positional argument in the context of functions in Python.

There are 2 ways you can solve this issue.

Solution to dataclass inheritance problem

The 1st solution uses the regular Python inheritance with some tricks.

We divide each of the classes into 2 classes where one class has non-default arguments, and another one has default arguments.

And then, when inheriting them in the child classes, we do so in such a way that when dataclass creates the __init__ class members, it does so in a way that default arguments come after non-default arguments.

@dataclass
class _UserBase:
    username: str
    email: str
    
@dataclass
class _UserDefaultBase(_UserBase): # [username, email, is_active]
    is_active: bool = True
    
@dataclass
class _AdminBase:
    admin_id: int
    
@dataclass
class _AdminDefaultBase(_UserDefaultBase): # [is_active, admin_level]
    admin_level: int = 1

@dataclass
class User(_UserDefaultBase, _UserBase): # [username, email, is_active]

    def deactivate_account(self):
        """Deactivates the user's account."""
        self.is_active = False
        print(f"{self.username}'s account has been deactivated.")

    def reactivate_account(self):
        """Reactivates the user's account."""
        self.is_active = True
        print(f"{self.username}'s account has been reactivated.")

@dataclass
class Admin(_AdminDefaultBase, User, _AdminBase):

    def promote_user(self, user: User):
        """Promotes a user by reactivating their account."""
        if self.admin_level == 1:
            user.reactivate_account()
            print(f"Admin {self.username} has promoted {user.username}.")
        else:
            print(f"You do not have admin access anymore.")

    def revoke_privileges(self):
        """Revokes admin privileges."""
        self.admin_level = 0
        print(f"{self.username} is no longer an admin.")

admin = Admin(898189, "Arup", "arup@thestartupcoder.com")
print(admin)

Output:
Admin(admin_id=898189, username='Arup', email='arup@thestartupcoder.com', is_active=True, admin_level=1)

It might look a little bit confusing on who is inheriting whom and what is the order, so I have created a diagram for that.

dataclass inheritance diagram

I have used some numbers to show you which class’s __init__ is called and in what sequence.

Now you might have a lot of questions related to this…

Why the admin has the sequence [admin_id, user_name, email, is_active, admin_level]?

How does this inheritance sequence creates this member list?

Let’s get the answers to these questions…

  • When the dataclass is being created by the @dataclass decorator, it looks through all of the class’s base classes in reverse MRO (that is, starting at object) and, for each dataclass that it finds, adds the fields from that base class to an ordered mapping of fields. After all of the base class fields are added, it adds its own fields to the ordered mapping. All of the generated methods will use this combined, calculated ordered mapping of fields. Because the fields are in insertion order, derived classes override base classes.
  • Taken from dataclass documentation

In short, the dataclass creates the members in the order from base to derived class. It follows the reverse MRO (Method Resolution Order).

If you are new to MRO, then you can refer to this documentation.

Though even if you do not read the documentation, I will try to make this as simple as possible.

Before finding the MRO of the inheritance tree, let’s simplify the names –

class A: # _UserBase
    pass

class B(A): # _UserDefaultBase
    pass

class C: # _AdminBase
    pass

class D(C): # _AdminDefaultBase
    pass

class E(B, A): # _User
    pass

class F(D, E, C): # Admin
    pass

To find the MRO, you need to know that linearization (L) means the sum of that class and `merge` of all parent class’s linearization + parent classes.

Simply, L[A(B)] = A + merge(L[B], B)

To understand the `merge`, you can see the following examples –

merge(A, A) = A
merge(AB, B) = A // B comes after A in one of the elements, so we took A as no element comes before it
merge(AO, BA) = B // A comes after B, O comes after A, so we took A as it does not come next to any class
merge(CO, EBAO, EC) = E // C, B, A, O comes after some class, we took E as it does not come after any other class

Let’s find the MRO now –

L[A] 
= AO (O => object)

L[C] 
= CO

L[B(A)] 
= B + merge(L[A], A) 
= B + merge(AO, A)
= BAO

L[D(C)] 
= D + merge(L[C], C) 
= D + merge(CO, C) 
= D + C + merge(O, O)
= DCO

L[E(B, A)] 
= E + merge(L[B], L[A], BA) 
= E + merge(BAO, AO, BA)
= E + B + merge(AO, AO, A)
= E + B + A + merge(O, O)
= EBAO

L[F(D, E, C)] 
= F + merge(L[D], L[E], L[C], DEC) 
= F + merge(DCO, EBAO, CO, DEC)
= F + D + merge(CO, EBAO, CO, EC)
= F + D + E + merge(CO, BAO, CO, C)
= F + D + E + B + merge(CO, AO, CO, C)
= F + D + E + B + C + merge(O, AO, O)
= F + D + E + B + C + A + merge(O, O, O)
= FDEBCAO

If you match the shortened names with the real names, then they will have the following sequence –

[Admin, AdminDefaultBase, User, UserDefaultBase, UserBase, AdminBase, object]

Now, you have the MRO, so we just reverse it to get the ordering of __init__ that the dataclass has used to initialize the members.

Does it make sense now why the members are ordered in that way? If you are still confused, then go through it again, I know it could be a little bit difficult to understand at first.

Another solution to `dataclass` inheritance

You can use kw_only=True to set any member used in the class as keyword argument.

from dataclasses import dataclass

@dataclass
class User:
    username: str
    email: str
    is_active: bool = True

    def deactivate_account(self):
        """Deactivates the user's account."""
        self.is_active = False
        print(f"{self.username}'s account has been deactivated.")

    def reactivate_account(self):
        """Reactivates the user's account."""
        self.is_active = True
        print(f"{self.username}'s account has been reactivated.")

@dataclass(kw_only=True)
class Admin(User):
    admin_id: int  # No default value
    admin_level: int = 1

    def promote_user(self, user: User):
        """Promotes a user by reactivating their account."""
        if self.admin_level == 1:
            user.reactivate_account()
            print(f"Admin {self.username} has promoted {user.username}.")
        else:
            print(f"You do not have admin access anymore.")

    def revoke_privileges(self):
        """Revokes admin privileges."""
        self.admin_level = 0
        print(f"{self.username} is no longer an admin.")
    
admin = Admin("Arup", "arup@thestartupcoder.com", admin_id=9891)
print(admin)

Output:
Admin(username='Arup', email='arup@thestartupcoder.com', is_active=True, admin_id=9891, admin_level=1)

It helps you in making sure that all the members will be treated as keyword arguments and then the ordering will not create any issue.

If you want to specify which field to treat as keyword argument, then you can do that as well.

from dataclasses import dataclass, field

@dataclass
class User:
    username: str
    email: str
    is_active: bool = True

    # ...

@dataclass
class Admin(User):
    admin_id: int = field(kw_only=True)
    admin_level: int = 1

    # ...

Conclusion

Dataclass inheritance is a powerful tool, but it comes with nuances that can trip you up, like the TypeError: non-default argument '...' follows default argument.

In this blog, we:

  • Explored how dataclass handles inheritance and constructs the __init__ method using reverse MRO.
  • Identified the root cause of the TypeError when mixing required and default fields.
  • Discussed two solutions:
    • Splitting classes into default and non-default parts for proper ordering.
    • Using kw_only=True to treat fields as keyword arguments and avoid ordering issues.

Actionable Steps:

  • Plan your dataclass hierarchy carefully, keeping the field order in mind.
  • Use kw_only=True for a straightforward fix if your project supports Python 3.10+.
  • Refer back to the MRO concept to understand field initialization order in inheritance.

With these strategies, you’ll avoid common pitfalls and leverage dataclass inheritance effectively in your projects!

Want to go to next concept in Python but not sure which one to pick? Read this blog where I discuss about all the essential concepts in Python that you must know.

References:

Hi, I’m Arup—a full-stack engineer at Enegma and a blogger sharing my learnings. I write about coding tips, lessons from my mistakes, and how I’m improving in both work and life. If you’re into coding, personal growth, or finding ways to level up in life, my blog is for you. Read my blogs for relatable stories and actionable steps to inspire your own journey. Let’s grow and succeed together! 🚀

Leave a Reply

Your email address will not be published. Required fields are marked *