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!
What Will You Learn? [hide]
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.

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 atobject
) and, for eachdataclass
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: