Table of Contents
Inheritance in Python
What is Inheritance?
Inheritance is a mechanism where a new class (called a child or subclass) inherits attributes and methods from an existing class (called a parent or superclass). This promotes code reuse: instead of rewriting code, the child class can extend or modify the parent’s behavior.
Key benefits:
- Reduces code duplication.
- Establishes a hierarchical relationship between classes.
- Enables extensibility (e.g., adding new features to child classes without altering the parent).
Basic Syntax
In Python, inheritance is defined by passing the parent class(es) as arguments to the child class definition:
class ParentClass:
# Parent attributes and methods
class ChildClass(ParentClass): # Child inherits from ParentClass
# Child-specific attributes and methods
Types of Inheritance
Python supports several types of inheritance, each suited to different scenarios.
Single Inheritance
A child class inherits from one parent class. This is the simplest and most common form.
Example:
Let’s define a Vehicle parent class with basic attributes (e.g., color) and methods (e.g., start_engine). A Car child class will inherit from Vehicle and add car-specific features (e.g., num_doors).
class Vehicle:
def __init__(self, color):
self.color = color # Attribute inherited by child classes
def start_engine(self):
print(f"{self.color} vehicle engine started.")
# Child class inheriting from Vehicle
class Car(Vehicle):
def __init__(self, color, num_doors):
super().__init__(color) # Initialize parent class
self.num_doors = num_doors # Child-specific attribute
def honk(self): # Child-specific method
print("Beep beep!")
# Usage
my_car = Car("red", 4)
my_car.start_engine() # Inherited method: "red vehicle engine started."
my_car.honk() # Child method: "Beep beep!"
print(my_car.num_doors) # Child attribute: 4
Multiple Inheritance and Method Resolution Order (MRO)
A child class inherits from two or more parent classes. Python uniquely supports multiple inheritance, but it introduces complexity: what if parent classes have methods with the same name?
To resolve conflicts, Python uses the Method Resolution Order (MRO)—a predefined order in which parent classes are checked for methods. You can view the MRO with ClassName.mro() or help(ClassName).
Example:
class Engine:
def start(self):
print("Engine starting...")
class Radio:
def start(self):
print("Radio turning on...")
# Child class inherits from both Engine and Radio
class Car(Engine, Radio):
pass
my_car = Car()
my_car.start() # Which parent's start() is called?
print(Car.mro()) # Output: [Car, Engine, Radio, object]
Why Engine first? MRO follows the “C3 linearization” algorithm, which prioritizes the order of parent classes in the child’s definition (Engine is listed before Radio). Thus, Car uses Engine.start().
Multilevel Inheritance
A child class inherits from a parent class, which in turn inherits from another class (forming a chain).
Example:
class Vehicle: # Grandparent
def move(self):
print("Moving...")
class Car(Vehicle): # Parent (inherits from Vehicle)
def wheels(self):
print("4 wheels")
class SportsCar(Car): # Child (inherits from Car)
def speed(self):
print("Max speed: 300 km/h")
# Usage
ferrari = SportsCar()
ferrari.move() # Inherited from Vehicle: "Moving..."
ferrari.wheels() # Inherited from Car: "4 wheels"
ferrari.speed() # Child method: "Max speed: 300 km/h"
Hierarchical Inheritance
One parent class is inherited by multiple child classes.
Example:
class Animal:
def breathe(self):
print("Breathing...")
class Dog(Animal):
def bark(self):
print("Woof!")
class Cat(Animal):
def meow(self):
print("Meow!")
# Usage
buddy = Dog()
whiskers = Cat()
buddy.breathe() # Inherited: "Breathing..."
buddy.bark() # Dog-specific: "Woof!"
whiskers.breathe() # Inherited: "Breathing..."
whiskers.meow() # Cat-specific: "Meow!"
Hybrid Inheritance
A combination of two or more types of inheritance (e.g., multilevel + multiple).
Example:
class A:
def method_a(self):
print("Method A")
class B(A): # Multilevel: B inherits from A
def method_b(self):
print("Method B")
class C(A): # Hierarchical: C also inherits from A
def method_c(self):
print("Method C")
class D(B, C): # Multiple: D inherits from B and C
pass
# MRO for D: [D, B, C, A, object]
d = D()
d.method_a() # Inherited from A via B/C
The super() Function
The super() function allows a child class to call methods from its parent class. It’s especially useful in __init__ to initialize parent attributes before adding child-specific ones.
Example:
class Parent:
def __init__(self, name):
self.name = name
print(f"Parent initialized with name: {self.name}")
class Child(Parent):
def __init__(self, name, age):
super().__init__(name) # Call Parent's __init__
self.age = age
print(f"Child initialized with age: {self.age}")
# Usage
child = Child("Alice", 10)
# Output:
# Parent initialized with name: Alice
# Child initialized with age: 10
In multiple inheritance, super() follows the MRO to determine which parent method to call.
Method Overriding
Method overriding occurs when a child class redefines a method inherited from the parent. This allows the child to provide custom behavior while retaining the method name.
Example:
class Bird:
def fly(self):
print("Bird flies.")
class Penguin(Bird):
def fly(self): # Override parent's fly()
print("Penguin cannot fly.")
# Usage
tweety = Bird()
tweety.fly() # "Bird flies."
penguin = Penguin()
penguin.fly() # "Penguin cannot fly." (overridden)
Access Modifiers and Inheritance
Python uses naming conventions to denote access levels for attributes/methods:
- Public: No leading underscores (e.g.,
name). Inherited freely. - Protected: Single leading underscore (e.g.,
_age). Intended for use within the class and its subclasses (not enforced, but a convention). - Private: Double leading underscores (e.g.,
__ssn). “Name-mangled” to_ClassName__ssn, making them hard to access from outside or subclasses.
Example:
class Parent:
def __init__(self):
self.public = "Public"
self._protected = "Protected"
self.__private = "Private"
class Child(Parent):
def access(self):
print(self.public) # OK (public)
print(self._protected) # OK (protected, by convention)
# print(self.__private) # Error: AttributeError (private)
child = Child()
child.access()
Polymorphism in Python
What is Polymorphism?
Polymorphism (from Greek: “many forms”) allows objects of different types to be treated as objects of a common type. For example, a function can work with multiple object types as long as they share a common method.
Types of Polymorphism
Runtime Polymorphism (Method Overriding)
We already saw this with method overriding! When a child class overrides a parent method, calling the method on a child object executes the child’s version. This is “runtime” because the method to call is determined at runtime based on the object’s type.
Example with a common interface:
class Shape:
def area(self):
pass # To be overridden by subclasses
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self): # Override area()
return 3.14 * self.radius **2
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self): # Override area()
return self.side** 2
# Polymorphic function: works with any Shape subclass
def print_area(shape):
print(f"Area: {shape.area()}")
# Usage
circle = Circle(5)
square = Square(4)
print_area(circle) # "Area: 78.5" (calls Circle.area())
print_area(square) # "Area: 16" (calls Square.area())
Compile-Time Polymorphism (Method Overloading)
Method overloading refers to defining multiple methods with the same name but different parameters (e.g., different number or types of arguments). Unlike languages like Java, Python does not natively support method overloading (due to dynamic typing). However, we can simulate it using:
- Default arguments
- Variable-length arguments (
*args/**kwargs)
Example with default arguments:
class Math:
def add(self, a, b, c=None):
if c is None:
return a + b
else:
return a + b + c
math = Math()
print(math.add(2, 3)) # 5 (uses 2 args)
print(math.add(2, 3, 4)) # 9 (uses 3 args)
Example with *args:
class Math:
def sum(self, *args):
return sum(args)
math = Math()
print(math.sum(1, 2)) # 3
print(math.sum(1, 2, 3, 4)) # 10
Duck Typing
Python follows the “duck typing” principle: “If it walks like a duck and quacks like a duck, it’s a duck.” This means an object’s suitability is determined by its methods/attributes, not its type.
Example:
class Duck:
def quack(self):
print("Quack!")
class RubberDuck:
def quack(self):
print("Squeak!")
class Dog:
def bark(self): # No quack() method
print("Woof!")
# Polymorphic function: works with any object that has quack()
def make_quack(duck):
duck.quack()
# Usage
make_quack(Duck()) # "Quack!" (Duck has quack())
make_quack(RubberDuck()) # "Squeak!" (RubberDuck has quack())
# make_quack(Dog()) # Error: AttributeError (Dog has no quack())
Abstract Base Classes (ABCs) and Interfaces
To enforce that subclasses implement specific methods (e.g., defining a common interface), use Python’s abc module to create abstract base classes (ABCs). An ABC contains one or more abstract methods (decorated with @abstractmethod), which subclasses must implement.
Example:
from abc import ABC, abstractmethod
class Shape(ABC): # Abstract base class
@abstractmethod
def area(self): # Abstract method (no implementation)
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self): # Must implement area()
return 3.14 * self.radius **2
class Square(Shape):
def __init__(self, side):
self.side = side
# def area(self): # Uncomment to see error (missing implementation)
# return self.side** 2
# Usage
circle = Circle(5)
print(circle.area()) # 78.5
# square = Square(4) # Error: Can't instantiate abstract class Square with abstract method area
Real-World Applications and Best Practices
Inheritance Best Practices
- Favor composition over inheritance when possible. Inheritance creates tight coupling; composition (e.g., including objects of other classes as attributes) is more flexible.
- Avoid deep inheritance hierarchies (e.g., great-grandparent → grandparent → parent → child). They become hard to maintain.
- Use ABCs to define clear interfaces for subclasses.
Polymorphism Best Practices
- Use polymorphism to write generic code (e.g., a function that works with all
Shapesubclasses). - Leverage duck typing for flexibility, but document expected methods/attributes.
- Avoid overloading when a single method with default arguments suffices.
Conclusion
Inheritance and polymorphism are foundational to Python OOP, enabling code reuse, flexibility, and scalability. Inheritance lets you build hierarchical class relationships, while polymorphism lets you treat objects of different types uniformly. By mastering these concepts, you’ll write cleaner, more maintainable code that adapts to changing requirements.
References
- Python Official Documentation: Inheritance
- Python Official Documentation: MRO
- Python Official Documentation: abc module
- “Fluent Python” by Luciano Ramalho (O’Reilly Media)
- “Python Crash Course” by Eric Matthes (No Starch Press)