Data Class Inheritance in Python
- Inheritance in Python
- Multi-Level Inheritance in Python
- Mix Default and Non-Default Attributes Among Base Class and Subclass Using Data Class Inheritance in Python
- Conclusion
Versions 3.7 and later introduced data class inheritance in Python. This article broadly explains multi-level inheritances and how to use the data class inheritance in Python.
Inheritance in Python
Data class inheritance in Python is used to get data in sub-classes from its parent class, which helps to reduce repeating codes and make code reusable.
Let’s look at an example of inheritance:
The program imports the dataclass
library package to allow the creation of decorated classes. The first class created here is Parent
, which has two member methods - string name
and integer age
.
Then a sub-class of Parent
is created here. The Child
class introduces a new member method - school
.
An instance object jack
is created for class Parent
, which passes two arguments to the class. Another instance object, jack_son
, is made for the Child
class.
As the Child
class is a subclass of Parent
, the data members can be derived in the Child
class. This is the main characteristic of data class inheritance in Python.
from dataclasses import dataclass
@dataclass
class Parent:
name: str
age: int
def print_name(self):
print(f"Name is '{self.name}' and age is= {self.age}")
@dataclass
class Child(Parent):
school: str
jack = Parent("Jack snr", 35)
jack_son = Child("Jack jnr", 12, school="havard")
jack_son.print_name()
Output:
C:\python38\python.exe "C:/Users/Win 10/PycharmProjects/class inheritance/2.py"
Name is 'Jack jnr' and age is= 12
Process finished with exit code 0
Multi-Level Inheritance in Python
As we have seen how data class inheritance in Python works, we will now look at the multi-level inheritance concept. It is a type of inheritance where a sub-class created from a parent class is used as a parent for the subsequent grandchild class.
The example below demonstrates multi-level inheritance in a simple form:
Parent Class
A class Parent
is created with a constructor __init__
, and a member method - print_method
. The constructor prints the statement "Initialized in Parent"
so that it gets displayed when the class is invoked from its child class.
The print_method
function has a parameter b
, which gets printed when this method is called.
Child Class
The Child
class is derived from Parent
and prints a statement inside its constructor. The super().__init__
refers to the base class with the child class.
We use super()
so that any potential cooperative multiple inheritances used by child classes will call the appropriate next parent class function in the Method Resolution Order (MRO).
The member method print_method
is overloaded, and a print statement prints the value of b
. Here too, super()
refers to the member method of its parent class.
Grand Child Class
At this point, the program just boilerplates (repeat codes) the structure to create another inherited class from the Child
class. Inside print_method
, the value of b
is incremented using super()
.
Main Function
At last, the main
function is created, which creates an object ob
and is made an instance of GrandChild()
. Lastly, the object ob
invokes the print_method
.
This is how multi-level classes are stacked when data class inheritance in Python is used.
class Parent:
def __init__(self):
print("Initialized in Parent")
def print_method(self, b):
print("Printing from class Parent:", b)
class Child(Parent):
def __init__(self):
print("Initialized in Child")
super().__init__()
def print_method(self, b):
print("Printing from class Child:", b)
super().print_method(b + 1)
class GrandChild(Child):
def __init__(self):
print("Initialized in Grand Child")
super().__init__()
def print_method(self, b):
print("Printing from class Grand Child:", b)
super().print_method(b + 1)
if __name__ == "__main__":
ob = GrandChild()
ob.print_method(10)
Output:
C:\python38\python.exe "C:/Users/Win 10/PycharmProjects/class inheritance/3.py"
Initialized in Grand Child
Initialized in Child
Initialized in Parent
Printing from class Grand Child: 10
Printing from class Child: 11
Printing from class Parent: 12
Process finished with exit code 0
Let’s understand what the code does here:
The main
function passes the value 10
to the print_method
function of the class Grand Child
. As per the MRO (Method Resolution Order), the program first executes the class Grand Child
, prints the __init__
statement, and then moves on to its parent - the Child
class.
The Child
class and Parent
class print their __init__
statements as per MRO, and then the compiler follows back to the print_method
of the GrandChild
class. This method prints 10
(the value of b
) and then uses super()
to increment the value of b
in its superclass, the Child
Class.
Then the compiler moves on to the print_method
of the Child
class and prints 11
. Then, in the last level of MRO is the Parent
class which prints 12
.
The program exits as no superclass exists over the Parent
class.
As we have understood how multi-level inheritance works in data class inheritance in Python, the next section will cover the concept of inheriting attributes from a parent class and how to modify it.
Mix Default and Non-Default Attributes Among Base Class and Subclass Using Data Class Inheritance in Python
We have seen how to use a child class to access data members of its parent class in data class inheritance in Python and how multi-level inheritance works. Now, a question arises if a child class can access data members of its superclass, can it make changes to it?
The answer is yes, but it must avoid TypeErrors. For example, in the below program, there are two classes, a Parent
class and a subclass Child
.
The class Parent
has three data members - name
, age
, and a bool variable ugly
which is set False
by default. Three member methods print the name
, age
, and id.
Now, inside the decorated Child
class derived from Parent
, a new data member is introduced - school
. With it, the class changes the attribute of the variable ugly
from False
to True
.
Two objects, jack
for Parent
and jack_son
for Child
, are created. These objects pass arguments to their classes, and both objects invoke the print_id
method and print the details.
A major problem with using this method to change default values of the base class is it causes a TypeError.
from dataclasses import dataclass
@dataclass
class Parent:
name: str
age: int
ugly: bool = False
def print_name(self):
print(self.name)
def print_age(self):
print(self.age)
def print_id(self):
print(f"ID: Name - {self.name}, age = {self.age}")
@dataclass
class Child(Parent):
school: str
ugly: bool = True
jack = Parent("jack snr", 32, ugly=True)
jack_son = Child("Mathew", 14, school="cambridge", ugly=True)
jack.print_id()
jack_son.print_id()
Output:
raise TypeError(f'non-default argument {f.name!r} '
TypeError: non-default argument 'school' follows default argument
A reason behind this error is that in data class inheritance in Python, the attributes cannot be used with defaults in a base class and then used without a default (positional attributes) in a subclass due to the way data classes mix attributes.
This is because the attributes are merged from the start at the MRO’s bottom and build up an ordered list of attributes in first-seen order, with overrides remaining in their original positions.
With ugly
as default, the Parent
begins with name
, age
, and ugly
, and then the Child
adds school
at the end of that list (with ugly
already in the list).
This results in having name
, age
, ugly
, and school
in the list, and since school
doesn’t have a default, the __init__
function lists the result as an incorrect parameter.
When the @dataclass
decorator creates a new Data Class, it searches through all of the class’s base classes in reverse MRO (starting at the object) and adds the fields from each base class to an ordered mapping of fields for each Data Class it finds.
It then adds its fields to the ordered mapping after all the base class fields have been added. This combined calculated ordered mapping of fields will be used by all created methods.
Because of the arrangement of the fields, derived classes supersede base classes.
If a field with no default value follows one with a default value, a TypeError will be generated. This is true whether it happens in a single class or due to class inheritance.
The first alternative to get around this problem is to force the fields with default values to a later position in the MRO order using different base classes. Avoid setting fields directly on classes that will be used as base classes, like Parent
, at all costs.
This program has base classes with fields, and fields with no defaults are separated. Public classes derive from base-with
and base-without
classes.
The sub-classes of the public class puts the base class upfront.
from dataclasses import dataclass
@dataclass
class _ParentBase:
name: str
age: int
@dataclass
class _ParentDefaultsBase:
ugly: bool = False
@dataclass
class _ChildBase(_ParentBase):
school: str
@dataclass
class _ChildDefaultsBase(_ParentDefaultsBase):
ugly: bool = True
@dataclass
class Parent(_ParentDefaultsBase, _ParentBase):
def print_name(self):
print(self.name)
def print_age(self):
print(self.age)
def print_id(self):
print(f"ID: Name - {self.name}, age = {self.age}")
@dataclass
class Child(_ChildDefaultsBase, Parent, _ChildBase):
pass
Amit = Parent("Amit snr", 32, ugly=True)
Amit_son = Child("Amit jnr", 12, school="iit", ugly=True)
Amit.print_id()
Amit_son.print_id()
Output:
C:\python38\python.exe "C:/main.py"
The Name is Amit snr and Amit snr is 32 year old
The Name is Amit jnr and Amit jnr is 12 year old
Process finished with exit code 0
The MRO created here prioritizes fields without defaults over fields with defaults by splitting up the fields to separate base classes with fields without defaults
and with defaults
and carefully choosing the inheritance order. The Child
’s MRO is:
<class 'object'>
||
<class '__main__._ParentBase'>,
||
<class '__main__._ChildBase'>
||
<class '__main__._ParentDefaultsBase'>,
||
<class '__main__.Parent'>,
||
<class '__main__._ChildDefaultsBase'>,
||
<class '__main__._Child'>
Although Parent
doesn’t create any new fields, it inherits the fields from ParentDefaultsBase
and shouldn’t come last in the field listing order. So, the ChildDefaultsBase
is kept at last to fulfill the correct order type.
The data class rules are also met since the classes ParentBase
and ChildBase
, which have fields without defaults, come before ParentDefaultsBase
and ChildDefaultsBase
, which have fields with defaults.
As a result, Child
is still a subclass of Parent
, whereas Parent
and Child
classes have a correct field order:
# __ Program Above __
print(signature(Parent))
print(signature(Child))
Output:
Conclusion
This article explains data class inheritance in Python in detail. Concepts like data class, child classes, multi-level inheritance, and mixing attributes from base class to subclass are explained thoroughly.
Related Article - Python Inheritance
Related Article - Python Class
- Python Generator Class
- How to Serialize a Python Class Object to JSON
- Python Abstract Property
- Python Class Factory
- Python Class Equality