Understanding Inheritance in Python: Core Concepts and Practical Examples

Mohammad Javad Mousavi
10 min readAug 25, 2024

--

flaticon.com

One of the core concepts in object-oriented programming (OOP) is inheritance. Inheritance allows you to create a hierarchy of classes where one class (called the derived class or child class) inherits the properties and methods of another class (called the base class or parent class). This powerful feature helps you model real-world relationships and allows for efficient code reuse and extension.

To simplify this concept, you can think of inheritance in programming as somewhat similar to genetic inheritance. Just as you inherit certain traits from your parents, a child class inherits attributes and behaviors from its parent class. This relationship enables the child class to share common functionality with the parent class while also having the ability to override or extend that functionality.

An Analogy: Genetic Inheritance vs. Object Inheritance

Imagine you inherited your hair color from your mother. This is an attribute you were born with. Now, suppose you decide to dye your hair purple. In this case, even though you initially inherited your natural hair color, you have overridden that attribute. Similarly, in object-oriented programming, derived classes can override certain properties and methods inherited from the parent class.

Additionally, you inherit, in a sense, your language from your parents. If your parents speak English, you likely speak English as well. But if you decide to learn a new language, like German, you are extending your attributes by adding a feature that your parents may not have. In programming, derived classes can inherit existing attributes and methods from the parent class while adding their own unique functionality.

Inheritance: Creating Relationships Between Classes

Inheritance in Python models a relationship between classes. This relationship means that when a derived class inherits from a base class, the derived class becomes a specialized version of the base class. The derived class takes on all the attributes and methods of the parent class but can also override or extend them as needed.

This mechanism not only helps in modeling real-world relationships effectively but also provides a simple and understandable structure for code reuse. Inheritance reduces development and maintenance costs by allowing you to add new features without changing the base class, and it helps keep your codebase clean and organized.

Note:

  • Classes that inherit from another class are called derived classes, subclasses, or subtypes.
  • Classes from which other classes are derived are called base classes or superclasses.
  • A derived class is said to inherit from or extend a base class.

Types of Inheritance in Python

In Python, there are several types of inheritance, which can be categorized as follows:

  1. Single Inheritance: In this type, a child class inherits from only one parent class. This is the simplest form of inheritance and is supported by many object-oriented languages.
  2. Multiple Inheritance: In this type, a child class can inherit from more than one parent class. This is a distinguishing feature of Python, as some other programming languages, such as Java, do not support multiple inheritance.
  3. Hierarchical Inheritance: In this type, multiple child classes inherit from a single parent class. This allows for a hierarchical structure where the base class provides common functionality to multiple derived classes.
  4. Multilevel Inheritance: In this type, a class inherits from another class, which itself is derived from a third class. This creates a chain of inheritance where a class indirectly inherits from multiple levels of ancestors.

Understanding these types of inheritance helps in leveraging the full power of object-oriented programming in Python and designing robust, reusable code structures.

https://github.com/pytopia/Python-2022/blob/main/Python/02.%20Object%20Oriented%20Programming/03%20Inheritance.ipynb

- Single Inheritance

Imagine we’re working on a project to create a simple drawing application. To make our application more useful, we need to handle different shapes, like rectangles and squares. Let’s think of these shapes as objects that we can work with programmatically.

To represent these shapes in our application, we need to create classes that describe them. Each class will encapsulate the properties and behaviors specific to that shape. For example, a rectangle has a length and a width, while a square has just one side length because all its sides are equal.

We want to create two classes:

  1. Rectangle: This class will store the dimensions of the rectangle and provide methods to calculate its area and perimeter.
  2. Square: This class will store the length of the side and provide methods to calculate its area and perimeter.

Here’s how we can implement these classes in code:

def Rectangle():
def __init__(self, length, width,):
self.length = length
self.width = width

def area(self):
return self.length * self.width

def perimeter(self):
retunr (2 * self.length) + (2 * self.width)
def Square():
def __init__(self, length,):
self.length = length

def area(self):
return self.length ** 2

def perimeter(self):
return 4 * self.length

In this example, you have two shapes that are related to each other: a square is a special kind of rectangle. The code, however, doesn’t reflect that relationship and thus has code that is essentially repeated.

By using inheritance, you can reduce the amount of code you write while simultaneously reflecting the real-world relationship between rectangles and squares:

Python Inheritance Syntax

In Python, inheritance allows one class (known as the derived class or child class) to inherit attributes and methods from another class (known as the base class or parent class). The syntax for implementing simple inheritance is straightforward:

class BaseClass:
# Body of the BaseClass
pass

class DerivedClass(BaseClass):
# Body of the DerivedClass
pass

Let’s apply the inheritance syntax to our example of shapes:

class Rectangle():
def __init__(self, length, width):
self.length = length
self.width = width

def area(self):
return self.length * self.width

def perimeter(self):
return (2 * self.length) + (2 * self.width)

class Square(Rectangle):
def __init__(self, length):
self.length = length
self.width = length

The code above has a significant issue. If the body of the parent class (Rectangle) is extensive, overriding methods in the child class (Square) can become problematic and error-prone. This is because you may inadvertently override methods or miss important functionality provided by the parent class.

To address this issue, it’s better to refactor the code to ensure that the base class (Rectangle) provides only the necessary functionality, and any specific behavior for derived classes (Square) is properly managed. This way, you reduce the risk of errors related to method overriding and ensure that the code remains clean and maintainable.

Here’s the revised code for clarity:

class Rectangle():
def __init__(self, length, width):
self.length = length
self.width = width

def area(self):
return self.length * self.width

def perimeter(self):
return (2 * self.length) + (2 * self.width)

class Square(Rectangle):
def __init__(self, length,):
Rectangle.__init__(self, length, length)

There’s still an issue: if the name of the parent class changes, you would need to update this change in the Square class as well. To resolve this problem, it’s better to use super(). This approach ensures that changes to the parent class name are automatically handled, making the code more maintainable and less prone to errors.

Here’s the revised code for clarity:

class Rectangle():
def __init__(self, length, width):
self.length = length
self.width = width

def area(self):
return self.length * self.width

def perimeter(self):
return (2 * self.length) + (2 * self.width)

class Square(Rectangle):
def __init__(self, length):
super().__init__( length, length)

Understanding the Python super() Function

The super() function is a built-in function in Python that provides a convenient way to access and delegate methods and attributes of parent classes. When used, it allows one class to access the methods and properties of another class in the same hierarchy. It is commonly used to avoid redundant code and make it more organized and easier to maintain.

Calling Parent Class’s __init__ Method:

  • In the Square class, the line super().__init__(length, length) is used in the constructor. This line calls the __init__ method of the Rectangle class, which is the parent class of Square.
  • The super() function is a way to refer to the parent class without naming it explicitly. It dynamically looks up the method in the parent class, making the code more flexible.

Why Use super()?

  • Maintainability: Using super() makes your code more maintainable. If the parent class name changes or if the class hierarchy becomes more complex, you don’t need to update the method calls manually. super() ensures that the correct parent class method is invoked regardless of changes.
  • Avoiding Redundancy: By using super(), you avoid duplicating code. The Square class leverages the __init__ method of the Rectangle class to handle the initialization of its attributes (length and width). This keeps the code DRY (Don't Repeat Yourself) and ensures that the initialization logic is centralized in one place.
  • Dynamic Method Resolution: super() is particularly useful in multiple inheritance scenarios. It dynamically resolves which method to call based on the class hierarchy, ensuring that the correct method from the parent class is used.

In summary, super() is a powerful tool for handling inheritance in Python. It simplifies method calls to parent classes, ensures that changes to the class hierarchy are handled gracefully, and helps keep the codebase clean and maintainable.

- Multi-Level Inheritance

https://github.com/pytopia/Python-2022/blob/main/Python/02.%20Object%20Oriented%20Programming/03%20Inheritance.ipynb

To create a class Cube that inherits from Square and extends the functionality of .area() (which was originally inherited from Rectangle through Square), we will override the .area() method to calculate the surface area of the cube. Additionally, we will add a method to calculate the volume of the cube.

Here’s the implementation:

class Cube(Square):

def area(self):
return 6 * super().area()

def volume(self):
return self.length ** 3

- Hierarchical Inheritance

Let’s create another class on top of everything named Shape which has two child classes Rectangle as before and Circle:

class Shape:
def __init__(self, color='Grey'):
self.color = color

class Circle(Shape):
"""
Circle class.
"""
def __init__(self, r, color='Red'):
super().__init__(color)
self.r = r

def area(self,):
return 3.14 * self.r * self.r

def perimeter(self,):
return 2 * 3.14 * self.r

class Rectangle(Shape):
def __init__(self, length, width, color='Green'):
super().__init__(color)
self.length = length
self.width = width

def area(self):
return self.length * self.width

def perimeter(self):
return 2 * self.length + 2 * self.width

# Here we declare that the Square class inherits from the Rectangle class
class Square(Rectangle):
def __init__(self, length, color='Yellow'):
super().__init__(length, length, color)

class Cube(Square):

def area(self):
return 6 * super().area()

def volume(self):
return self.length ** 3

- Abstract base class

In Python, abstract base classes (ABCs) are a powerful tool provided by the abc module. They allow you to define classes that are intended to be inherited but not directly instantiated. This is helpful in scenarios where you want to enforce a structure across all subclasses, ensuring that certain methods or properties are implemented by the inheriting classes.

What Is an Abstract Base Class?

An abstract base class is a class that cannot be instantiated on its own and only serves as a blueprint for other classes. The abstract methods defined in an ABC must be implemented by its child classes. This enforces a contract that guarantees the child classes will have certain methods.

To define an abstract base class in Python, you use the ABC class from the abc module, and decorate any methods you want to be abstract with @abstractmethod.

Here’s an example based on the concept of geometric shapes:

from abc import ABC, abstractmethod

class Shape(ABC):
"""
Abstract shape class.
"""
def __init__(self, color='Black'):
print("Shape constructor called!")
self.color = color

def __str__(self):
return f"Shape is {self.color}"

@abstractmethod
def area(self):
pass

@abstractmethod
def perimeter(self):
pass
  • Inheriting from ABC:
  • The Shape class inherits from ABC, making it an abstract base class. This means that Shape itself cannot be instantiated. You cannot create a direct instance of Shape—it exists only to be subclassed.
  • Abstract Methods:
  • Methods marked with @abstractmethod such as area() and perimeter() have no implementation in the Shape class. These methods must be implemented in any subclass that inherits from Shape. If a subclass does not implement these methods, it will raise a TypeError.
  • Preventing Instantiation:
  • If you try to create an instance of the Shape class directly, Python will raise an error:
shape = Shape()  # This will raise a TypeError

The error will say something like:

TypeError: Can't instantiate abstract class Shape with abstract methods area, perimeter
  • This is because Shape is abstract and cannot be instantiated until the abstract methods have been implemented.
  • Initialization of Attributes:
  • The Shape class has a constructor (__init__()) that initializes a color attribute. This is a concrete method that will be inherited by all child classes, meaning that all shapes will have a color attribute, even though the class is abstract.
  • String Representation:
  • The __str__() method is implemented to return the shape’s color. This method is not abstract, so it will be inherited by all subclasses, allowing them to have a string representation without needing to implement their own __str__() method unless they choose to override it.

Why Use Abstract Base Classes?

  1. Enforcing Structure:
  • ABCs enforce that certain methods or properties must be present in subclasses. This is useful in scenarios where you want to ensure that all child classes adhere to a specific interface.

2. Clearer Design:

  • By defining abstract methods, you make it clear to other developers (or your future self) that certain functionality must be implemented in any class that inherits from the ABC.

3. Preventing Incorrect Instantiation:

  • The use of ABCs prevents the accidental instantiation of classes that are not meant to be instantiated, which could lead to bugs.
flaticon.com

I would like to extend my heartfelt thanks to Ali Hejazi for his invaluable contributions to the GitHub repository pytopia. The insights and information provided in the repository have been instrumental in shaping my understanding and presentation of programming paradigms. Your dedication to sharing knowledge is deeply appreciated and has significantly enriched the content shared here. Thank you for your hard work and generosity in making such resources available to the community.

For further resources and information, please visit the GitHub repository pytopia: pytopia on GitHub.

I would be delighted to hear your feedback or any suggestions you might have after reading this content. Your input is highly valuable and will help in improving the materials shared.

--

--

Mohammad Javad Mousavi

Mohammad Javad | MSc Student in Biomedical Engineering (Bioelectric) | Interested in Neuroscience & Neuroimaging, focused on brain data analysis.