Previous | Tutorial index | Next
Understand and implement inheritance relationships between classes.
Inheritance is a mechanism that allows a new class (called a subclass or child class) to acquire the properties and behaviours of an existing class (called a superclass or parent class). The subclass is a specialized version of the superclass—it is‑a more specific type of the parent.
B inherits from class A, we say that B is‑a A.Dog is‑a Animal.Car is‑a Vehicle.SavingsAccount is‑a BankAccount.Square is‑a Shape.This relationship reflects how we naturally categorise things in the real world. Inheritance models this hierarchical classification in code.
To declare a subclass, you place the superclass name in parentheses after the subclass name:
class Superclass:
# ... class body ...
class Subclass(Superclass):
# ... class body ...
class Animal:
def __init__(self, name):
self.name = name
def eat(self):
return f"{self.name} is eating."
def sleep(self):
return f"{self.name} is sleeping."
class Dog(Animal):
# Dog inherits __init__, eat(), and sleep() automatically
pass
# Creating instances
fido = Dog("Fido")
print(fido.eat()) # Fido is eating. (inherited)
print(fido.sleep()) # Fido is sleeping. (inherited)
print(fido.name) # Fido (inherited attribute)
Even though the Dog class is empty (pass), it automatically has all the attributes and methods defined in Animal. This is the power of inheritance.
Inheritance can be chained to create deeper hierarchies.
class Animal:
def __init__(self, name):
self.name = name
class Mammal(Animal):
def __init__(self, name, fur_color):
super().__init__(name) # Call Animal's __init__
self.fur_color = fur_color
def nurse_young(self):
return f"{self.name} is nursing."
class Dog(Mammal):
def __init__(self, name, fur_color, breed):
super().__init__(name, fur_color) # Call Mammal's __init__
self.breed = breed
def bark(self):
return f"{self.name} says woof!"
# Now Dog inherits from Mammal, which inherits from Animal.
# Dog has: name, fur_color, breed, eat(), sleep(), nurse_young(), bark().
When you inherit from a superclass, the subclass automatically receives:
__init__ (provided you call super().__init__()).__str__).__ double leading underscore) are name‑mangled and not directly accessible in subclasses (though they still exist in the instance).__init__ method is inherited but is often overridden to add new attributes.class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def start(self):
return "Engine started."
def stop(self):
return "Engine stopped."
class Car(Vehicle):
def __init__(self, make, model, year, doors):
super().__init__(make, model, year)
self.doors = doors
def honk(self):
return "Beep beep!"
my_car = Car("Toyota", "Camry", 2022, 4)
print(my_car.start()) # Engine started. (inherited)
print(my_car.honk()) # Beep beep! (new method)
print(my_car.make) # Toyota (inherited attribute)
Sometimes, a subclass needs to provide a different implementation of a method that already exists in the superclass. This is called method overriding.
Simply define a method with the same name in the subclass. The subclass's version will be called instead of the superclass's version.
class Animal:
def speak(self):
return "Some generic animal sound."
class Dog(Animal):
def speak(self): # Override the superclass method
return "Woof!"
class Cat(Animal):
def speak(self): # Override again
return "Meow!"
animals = [Animal(), Dog(), Cat()]
for a in animals:
print(a.speak())
Output:
Some generic animal sound.
Woof!
Meow!
You can still access the superclass's version using super() (see Section 4.5).
Often, you don't want to completely replace the superclass method—you want to add to it. This is called method specialization or extension.
super() to Call the Parent's MethodThe super() function returns a proxy object that allows you to call methods from the superclass. This is essential for extending behaviour.
class Animal:
def speak(self):
return "Animal sound"
class Dog(Animal):
def speak(self):
parent_sound = super().speak() # Call parent method
return f"Dog says: {parent_sound} and then barks!"
d = Dog()
print(d.speak()) # Dog says: Animal sound and then barks!
__init__When you add new attributes in a subclass, you must call the superclass's __init__ to ensure that inherited attributes are properly initialized.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
class Student(Person):
def __init__(self, name, age, student_id):
super().__init__(name, age) # Initialize name and age
self.student_id = student_id # New attribute
s = Student("Alice", 20, "S123")
print(s.name) # Alice
print(s.student_id) # S123
If you forget super().__init__(), the name and age attributes would not be created, leading to an AttributeError when you try to access them.
super() Function in Depthsuper() is a built‑in function that returns a temporary object of the superclass, allowing you to call its methods. It is used extensively in inheritance to avoid hardcoding the superclass name, making code more maintainable and supporting multiple inheritance.
super().__init__() – inside a method, calls the superclass's method (most common).super(Subclass, self).method() – the older, explicit form (rarely needed now).super() Instead of the Superclass Name?super() follows the Method Resolution Order (MRO) correctly, which is crucial when a class inherits from multiple parents (more on that later).super() ensures that all parent methods are called in the correct order.super() in a Chainclass A:
def __init__(self):
print("A init")
super().__init__() # Even though A has no parent, this is safe
class B(A):
def __init__(self):
print("B init")
super().__init__()
class C(B):
def __init__(self):
print("C init")
super().__init__()
c = C()
# Output:
# C init
# B init
# A init
Notice that super() in A does nothing (calls object.__init__) but doesn't raise an error. This is why it's safe to call super().__init__() even in the topmost class.
When a method is called on an instance, Python searches for it in a specific order: the instance's class, then its parent class, then the parent's parent, and so on, up to object (the ultimate base class). This search order is called the Method Resolution Order (MRO).
You can inspect the MRO of any class using the __mro__ attribute or the mro() method.
class Animal: pass
class Mammal(Animal): pass
class Dog(Mammal): pass
print(Dog.__mro__)
# (<class '__main__.Dog'>, <class '__main__.Mammal'>, <class '__main__.Animal'>, <class 'object'>)
super(), Python uses the MRO to decide which class's method to call next.Python supports multiple inheritance, where a subclass can inherit from more than one superclass.
class Flyer:
def fly(self):
return "Flying..."
class Swimmer:
def swim(self):
return "Swimming..."
class Duck(Flyer, Swimmer):
pass
d = Duck()
print(d.fly()) # Flying...
print(d.swim()) # Swimming...
When two superclasses have a method with the same name, the MRO determines which one is used. Python uses the C3 linearization algorithm to create a consistent MRO.
class A:
def method(self):
print("A")
class B(A):
def method(self):
print("B")
class C(A):
def method(self):
print("C")
class D(B, C):
pass
d = D()
d.method() # Output: B (because B appears before C in MRO)
print(D.__mro__) # (D, B, C, A, object)
Best Practice: Use multiple inheritance sparingly. Prefer composition over inheritance when possible. If you do use it, be aware of the MRO and design your classes cooperatively (always call super()).
| Pitfall | Explanation | How to Avoid |
|---|---|---|
Forgetting to call super().__init__() |
Inherited attributes are not initialized, leading to AttributeError. |
Always call super().__init__() in __init__ of every subclass. |
Calling super() in the wrong place |
Placing super() after assigning subclass attributes can skip parent initialization. |
Call super().__init__() at the beginning of your __init__ (or at least before using inherited attributes). |
Overriding a method without calling super() when needed |
You lose the parent's behaviour when you intended to extend it. | If you want to extend, use super().method() inside your override. |
| Using multiple inheritance without understanding MRO | Method calls can become unpredictable. | Avoid complex multiple inheritance; use mixins for simple, focused behaviour. |
| Changing the superclass name | Hardcoding ParentClass.method(self) breaks if you rename the parent. |
Always use super() instead of hardcoding the parent class name. |
| Creating deep inheritance hierarchies | Overly deep hierarchies become fragile and hard to maintain. | Prefer shallow hierarchies; use composition (has‑a) when appropriate. |
Let's build a complete example with inheritance, method overriding, and super().
import math
class Shape:
"""Base class for all shapes."""
def __init__(self, color="black"):
self.color = color
def area(self):
raise NotImplementedError("Subclasses must implement area()")
def perimeter(self):
raise NotImplementedError("Subclasses must implement perimeter()")
def __str__(self):
return f"{self.__class__.__name__} (color={self.color})"
class Rectangle(Shape):
def __init__(self, width, height, color="black"):
super().__init__(color) # Initialize color
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
def __str__(self):
return f"Rectangle(width={self.width}, height={self.height}, color={self.color})"
class Square(Rectangle):
def __init__(self, side, color="black"):
super().__init__(side, side, color) # Reuse Rectangle's __init__
def __str__(self):
return f"Square(side={self.side}, color={self.color})"
@property
def side(self):
return self.width # Since width and height are equal
class Circle(Shape):
def __init__(self, radius, color="black"):
super().__init__(color)
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
# --- Using the hierarchy ---
shapes = [
Rectangle(5, 3, "red"),
Square(4, "blue"),
Circle(2.5, "green")
]
for shape in shapes:
print(shape)
print(f" Area: {shape.area():.2f}")
print(f" Perimeter: {shape.perimeter():.2f}\n")
Output:
Rectangle(width=5, height=3, color=red)
Area: 15.00
Perimeter: 16.00
Square(side=4, color=blue)
Area: 16.00
Perimeter: 16.00
Circle(radius=2.5, color=green)
Area: 19.63
Perimeter: 15.71
Key Observations:
Square inherits from Rectangle, reusing its __init__ and methods.Shape uses abstract methods (raising NotImplementedError) to enforce that subclasses implement area() and perimeter().__str__ to provide a meaningful representation.super().__init__() is used consistently to propagate initialization.super()Answer the following questions to check your understanding.
1. What is the correct syntax for declaring a class Dog that inherits from Animal?
class Dog inherits Animal:class Dog(Animal):class Dog extends Animal:class Dog -> Animal:2. When a subclass overrides a method, the subclass's version:
super().super() is used.3. What does super().__init__() do in a subclass?
4. Consider the following code:
class A:
def __init__(self):
self.x = 1
class B(A):
def __init__(self):
self.y = 2
b = B()
print(b.x)
What happens?
x was never initialized5. (True/False) Private attributes (with double underscore __) are inherited by subclasses.
6. Which of the following is a valid use of super()?
super().__init__()super().method_name()7. The Method Resolution Order (MRO) determines:
8. What is the output of the following code?
class Parent:
def greet(self):
return "Hello from Parent"
class Child(Parent):
def greet(self):
return "Hello from Child"
c = Child()
print(c.greet())
Part A: Create a Class Hierarchy for Employees
Design three classes:
Employee (superclass):
name (str), employee_id (int), base_salary (float).calculate_salary() – returns the base salary (for now). display() – returns a string with employee details.Manager (subclass of Employee):
bonus (float).calculate_salary() to return base_salary + bonus.display() to include bonus information.Developer (subclass of Employee):
projects_completed (int), project_bonus (float per project, default 1000).calculate_salary() to return base_salary + (projects_completed * project_bonus).display() to include project info.Part B: Instantiate and Test
Create one Employee, one Manager, and one Developer. Call display() and calculate_salary() on each. Ensure you use super() correctly to avoid duplicating code.
Part C: Reflection Question
In your code comments, answer: "Why is it beneficial to have calculate_salary() defined in Employee and overridden in subclasses rather than having completely separate methods?" (Hint: Polymorphism.)
Task: You are developing a software system for a zoo. Create a complete Python script that models animals using inheritance. Follow the specifications below.
Base Class: Animal
name (str), species (str), age (int), diet (str, e.g., "carnivore", "herbivore"), weight_kg (float).eat() – returns a string like "Tiger is eating meat." (diet‑specific).sleep() – returns "{name} is sleeping.".make_sound() – returns a generic string "Some animal sound" (to be overridden).display_info() – returns a formatted string with all attributes.Subclass: Mammal (inherits from Animal)
fur_color (str).make_sound() to return "Mammal sound" (generic).nurse() – returns "{name} is nursing its young.".Subclass: Bird (inherits from Animal)
wing_span_cm (float).make_sound() to return "Chirp!".fly() – returns "{name} is flying.".Subclass: Reptile (inherits from Animal)
is_venomous (bool).make_sound() to return "Hiss!".shed_skin() – returns "{name} shed its skin.".More Specific Subclasses (choose at least 2 of the following):
Lion (inherits from Mammal) – overrides make_sound() to "Roar!".Elephant (inherits from Mammal) – overrides make_sound() to "Trumpet!".Parrot (inherits from Bird) – overrides make_sound() to "Squawk!" and adds a method speak(word) that returns a string.Eagle (inherits from Bird) – overrides make_sound() to "Screech!".Snake (inherits from Reptile) – overrides make_sound() to "Sssss!".Lizard (inherits from Reptile) – overrides make_sound() to "Click!".Part 1: Code
Write the complete class definitions with all methods. Include:
super() in __init__ for each subclass.Part 2: Testing Script
In the same file, create a list called zoo_animals containing at least 6 animal objects (from different subclasses). Then:
display_info() on each.make_sound().nurse().fly().shed_skin().eat() and sleep() on all animals, showing that the correct version executes for each.Part 3: Reflection Questions
Answer these in a comment block at the top of your script:
Penguin class? Would it inherit from Bird or something else? Why?super().__init__() in the Mammal class? How would that affect Lion?Animal class useful even though you will never instantiate it directly? (Hint: abstraction and polymorphism.)walk() to Animal, how many subclasses would automatically get it? Explain.Bonus Challenge (Optional):
Add a class attribute total_animals to Animal that increments every time an animal is instantiated. Add a class method get_total_animals() that returns the count. Demonstrate that it works correctly.
super().Before moving to Tutorial 5 (Access Modifiers), ensure you can confidently say YES to the following:
Ready for the next tutorial? In Tutorial 5, you will learn about encapsulation and access modifiers—how to use naming conventions to control access to class members and protect data integrity.