Data Class Inheritance in Python

Jay Shaw Oct 10, 2023
  1. Inheritance in Python
  2. Multi-Level Inheritance in Python
  3. Mix Default and Non-Default Attributes Among Base Class and Subclass Using Data Class Inheritance in Python
  4. Conclusion
Data Class Inheritance in Python

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:

Signature Function

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

Related Article - Python Dataclass