Table of Contents | |
What’s Inheritance in Python?
Python Inheritance is a pivotal concept in object-oriented programming (OOP) that allows a class to inherit attributes and methods from another class. When one class inherits from another, it takes on the attributes and methods of the first class. The original class is called the parent class, and the new class is the child class. The child class can inherit any or all of the attributes and methods of its parent class, but it’s also free to define new attributes and methods of its own.
This mechanism facilitates code reuse and the creation of hierarchical relationships between classes. To inherit a class, we need to put the name of the class we’re inheriting between the parentheses after the name of our subclass. The syntax for single inheritance with one parent class is shown below.
Syntax of Python Inheritance
class child_class_name(parent_class_name):
statements
In Python inheritance, the terminology can be a bit confusing, but here’s the breakdown:
- Class that provides features and behaviors is called the parent class, base class, or superclass.
- Class that inherits these features and behaviors is called the child class, derived class, or subclass.
Example of Python Inheritance
Let’s make the Dog class a subclass of the Animal class. Animal is called the parent or superclass of Dog. The relationship between subclass and superclass (inheritance) is shown below.

class Animal: # Parent (or base) class
def __init__(self, name):
self.name = name
def speak(self):
print("Generic animal sound")
class Dog(Animal): # Child (or derived) class
def speak(self):
print("Woof!")
Explanation
- class
Animal
: Defines a general Animal class. It has a name attribute and a speak method. - class
Dog(Animal)
: Creates a Dog class that inherits from Animal. This means a Dog object automatically gets the name attribute and speak method from the Animal class. def speak(self)
: Inside the Dog class, we override (change) the speak method. Now, when a Dog object speaks, it says “Woof!” instead of the generic animal sound.
Using the super()
Function
There are two requirements for using an inherited class in Python.
The first requirement is defining the inheritance hierarchy, which you do by giving the class inherited from, in parentheses, immediately after the name of the class being defined with the class keyword. Dog inherits from Animal class.
The second and more subtle element is explicitly calling the __init__ method of inherited class. Python doesn’t automatically do this for you, but you can use the super function to have Python figure out which inherited class to use. This task is accomplished in the example code line 11. This code calls the Animal class being initialized with the appropriate arguments. Otherwise, instance of Dog wouldn’t have their name and sound instance variables set.
Example
class Animal: # Parent class
def __init__(self, name, sound):
self.name = name
self.sound = sound
def speak(self):
print(f"{self.name} says {self.sound}")
class Dog(Animal): # Child class
def __init__(self, name, breed):
super().__init__(name, "Woof!") # Calling parent's __init__
self.breed = breed
def fetch(self):
print(f"{self.name} the {self.breed} is fetching the ball!")
my_dog = Dog("Buddy", "Golden Retriever")
my_dog.speak() # Output: Buddy says Woof!
my_dog.fetch() # Output: Buddy the Golden Retriever is fetching the ball!
The parent class (Animal) has general traits, while the child class (Dog) inherits those traits but adds its own unique twist. The super() function allows the child class to “borrow” features (methods) from its parent.
Explanation
- class
Animal
: Parent class, representing a generic animal. It has attributes name and sound, and a method speak to express the sound the animal makes. - class
Dog(Animal)
: Child class, inheriting from the Animal class. This means dogs get all the traits of a generic animal, but we can also add dog-specific features. super().__init__(name, "Woof!")
: Inside the Dog class’s constructor (__init__), we use super() to call the constructor of the parent Animal class. This sets the name and sound attributes for the new Dog object, ensuring it starts with basic animal characteristics. Notice how we pass “Woof!” as the default sound for a dog.self.breed = breed
: Sets a unique attribute for dogs, their breed.- Creating and Using: We create a Dog object named my_dog. When we call my_dog.speak(), it uses the speak method inherited from Animal (but with the dog’s specific sound). We also have a new fetch method, only available to dogs!
Key Points
- super() helps avoid repeating code that’s already in the parent class.
- It makes your child classes more connected to the parent, promoting code organization.
- super() is super handy for setting up shared attributes and adding specific features.
The __init__()
Method for a Child Class
When you’re writing a new class based on an existing class, you’ll often want to call the __init__() method from the parent class. This will initialize any attributes that were defined in the parent __init__() method and make them available in the child class.
As an example, let’s model an electric car. An electric car is just a specific kind of car, so we can base our new ElectricCar class on the Car class. Then we’ll only have to write code for the attributes and behaviors specific to electric cars.
Let’s start by making a simple version of the ElectricCar class, which does everything the Car class does:
Example
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def start_car(self):
print(f"{self.make} {self.model} started.")
class ElectricCar(Car):
def __init__(self, make, model, year):
super().__init__(make, model, year)
ev = ElectricCar("Tesla", "Model 3", 2023)
ev.start_car() # Output: Tesla Model 3 started.
We start with parent Car class. We then define the child class, ElectricCar. The name of the parent class must be included in parentheses in the definition of a child class. The __init__() method takes in the information required to make a Car instance.
The super() function is a special function that allows you to call a method from the parent class. Line 12 tells Python to call the __init__() method from Car, which gives an ElectricCar instance all the attributes defined in that method.
To test inheritance working properly we create an electric car with the same kind of information we’d provide when making a regular car. We make an instance of the ElectricCar class and assign it to ev. Line 14 calls the __init__() method defined in ElectricCar, which tells Python to call the __init__() method defined in the parent class Car. We provide the arguments ‘Tesla‘, ‘Model 3‘, and 2023.
The ElectricCar instance works just like an instance of Car, so now we can begin defining attributes and methods specific to electric cars.
Defining Attributes and Methods for the Child Class
Once you have a child class that inherits from a parent class, you can add any new attributes and methods necessary to differentiate the child class from the parent class.
Let’s add an attribute specific to electric cars (a battery, for example) and a method to report on this attribute. We’ll store the battery size and write a method that prints a description of the battery:
Example
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def start_car(self):
print(f"{self.make} {self.model} started.")
class ElectricCar(Car):
def __init__(self, make, model, year):
super().__init__(make, model, year)
self._battery_size = 82 # Protected attribute
def describe_battery(self):
print(f"This car has an {self._battery_size}-kWh battery.")
ev = ElectricCar("Tesla", "Model 3", 2023)
ev.start_car() # Output: Tesla Model 3 started.
ev.describe_battery() # Output: This car has an 82-kWh battery.
New lines added in the code: 13, 15, 16, 20
We add a new attribute self._battery_size and set its initial value to 82. This attribute will be associated with all instances created from the ElectricCar class but won’t be associated with any instances of Car. We also add a method called describe_battery() that prints information about the battery. When we call this method, we get a description specific to an electric car: This car has an 82-KWh battery.
An attribute or method that could belong to any car, rather than one that’s specific to an electric car, should be added to the Car class instead of the ElectricCar class. Then anyone who uses the Car class will have that functionality available as well, and the ElectricCar class will only contain code for the information and behavior specific to electric vehicles.
Overriding Methods from the Parent Class
You can override any method from the parent class that doesn’t fit what you’re trying to model with the child class. To do this, you define a method in the child class with the same name as the method you want to override in the parent class. Python will disregard the parent class method and only pay attention to the method you define in the child class.
Say the class Car had a method called fill_fuel(). This method is meaningless for an all-electric vehicle, so you might want to override it. Here’s one way to do that:
Example
class Car:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def start_car(self):
print(f"{self.make} {self.model} started.")
def fill_fuel(self): # Made generic for any fuel type
print(f"Filling up the {self.make}'s fuel tank.")
class ElectricCar(Car):
def __init__(self, make, model, year):
super().__init__(make, model, year)
self._battery_size = 82 # Protected attribute
def describe_battery(self):
print(f"This car has an {self._battery_size}-kWh battery.")
def fill_fuel(self): # Overriding the parent's fill_fuel method
print(f"{self.make} doesn't need gasoline. Charging the battery...")
ev = ElectricCar("Tesla", "Model 3", 2023)
ev.start_car() # Output: Tesla Model 3 started.
ev.describe_battery() # Output: This car has an 82-kWh battery.
ev.fill_fuel() # Output: Tesla doesn't need gasoline. Charging the battery...
New lines added in the code: 10, 11, 21, 22, 27
Now if someone tries to call fill_fuel() with an electric car, Python will ignore the method fill_fuel() in Car and run this code instead. When you use inheritance, you can make your child classes retain what you need and override anything you don’t need from the parent class.
Key Points
- Method overriding lets you change how a method works in a child class.
- The child class’s method has the same name as the parent’s method.
- This allows you to customize behavior inherited from the parent class.
- Python will always use the child’s overridden method when you call it on an object of that child class.
Types of Python Inheritance
Python’s flexible syntax supports multiple inheritance. This allows you to create a class that inherits from two or more parent classes.
class child_class(parent_class1, parent_class2, ...):
statements
Inheritance in Python lets you create new classes (child classes) that inherit features from existing ones (parent classes). It’s like a family tree, where children inherit traits from their parents. Let’s look at the different types:
Single Inheritance
A child class inherits from only one parent class.
Example
class Animal: # Parent class
def speak(self):
print("Animal sound")
class Dog(Animal): # Child class
def speak(self):
print("Woof!")
my_dog = Dog()
my_dog.speak() # Output: Woof!
Explanation
- class
Animal
: Defines a basic animal with a speak method. - class
Dog(Animal)
: The Dog class inherits from Animal. It has its own speak method, overriding the parent’s method. my_dog = Dog()
: Creates a Dog object.my_dog.speak()
: Calls the speak method of the Dog object, which prints “Woof!”
Multiple Inheritance
A child class inherits from multiple parent classes.
Example
class Flyer:
def fly(self):
print("Flying")
class Swimmer:
def swim(self):
print("Swimming")
class Duck(Flyer, Swimmer): # Child class inherits from two parent classes
pass
my_duck = Duck()
my_duck.fly() # Output: Flying
my_duck.swim() # Output: Swimming
Explanation
- class
Flyer
and classSwimmer
: Define classes with single methods fly and swim. - class
Duck(Flyer, Swimmer)
: The Duck class inherits from both Flyer and Swimmer. my_duck = Duck()
: Creates a Duck object.my_duck.fly() and my_duck.swim()
: Calls the inherited methods.
Multilevel Inheritance
A child class inherits from a parent class, which itself inherits from another parent class, creating a chain.
Example
class Animal: # Base class
def speak(self):
print("Animal sound")
class Dog(Animal): # First level child class
def speak(self):
print("Woof!")
class Bulldog(Dog): # Second level child class (inherits from Dog)
pass
my_bulldog = Bulldog()
my_bulldog.speak() # Output: Woof!
Explanation
- class
Bulldog(Dog)
: Bulldog inherits from Dog, which inherits from Animal. my_bulldog.speak()
: Calls the speak method. Since Bulldog doesn’t have its own speak, it uses the one from Dog.
Python Inheritance and Access Modifiers
Inheritance is like a family tree, where child classes inherit traits from their parents. But not all family secrets are meant to be shared! Access modifiers act like privacy settings for these traits (attributes and methods), controlling who can see and change them.
Example
class Animal: # Parent class
def __init__(self, name):
self.name = name # Public attribute
self._sound = "Generic animal sound" # Protected attribute
self.__energy = 100 # Private attribute (double underscores)
def speak(self):
print(self._sound)
def __sleep(self): # Private method
print("Zzz...")
self.__energy += 10
class Dog(Animal): # Child class
def speak(self):
print("Woof!")
self._Animal__sleep() # Accessing and calling private method of parent class
my_dog = Dog("Buddy")
print(my_dog.name) # Output: Buddy
# print(my_dog._sound) # This would cause an error
# print(my_dog.__energy) # This would also cause an error
my_dog.speak() # Output: Woof!\nZzz...
Explanation
- Public Modifier (no underscores):
self.name = name
: The name attribute is public, accessible from anywhere.
- Protected Modifier (single underscore):
self._sound = "Generic animal sound"
: The _sound attribute is protected. It’s accessible within the Animal class and its subclasses like Dog, but not from outside.
- Private Modifier (double underscores):
self.__energy = 100
: The __energy attribute is private, accessible only within the Animal class itself. Not even Dog can access it directly.
- Private Method:
def __sleep(self)
: The __sleep method is also private and can only be accessed within the Animal class. However, the child class can access and call the private method using _ParentClassName__MethodName format.
- Method Overriding:
- The Dog class overrides the speak method, giving it its own specific behavior.
- Inside the overridden speak() method, we call the private method __sleep() using _Animal__sleep() format.
- Accessing Attributes:
print(my_dog.name)
: Access the public name attribute of my_dog.- Trying to access my_dog._sound or my_dog.__energy directly would cause an error.
Key Points
- Public: No underscores, accessible from anywhere.
- Protected: Single underscore, accessible within the class and its subclasses.
- Private: Double underscores, accessible only within the class itself.
Why use Access Modifiers?
- Encapsulation: Bundles data (attributes) and behaviors (methods) together, protecting the internal workings of your class.
- Data Hiding: Prevents accidental modification of important attributes.
- Maintainability: Makes your code easier to understand and change later.
While Python doesn’t strictly enforce these access modifiers, they serve as guidelines for developers, promoting good coding practices and preventing accidental modification of data.
Complete Example of Python Inheritance
Imagine you’re building a school information system to keep track of teachers and students. Both teachers and students share some basic information, like having a name, age, and address. However, they also have their unique details: teachers have salaries and courses they teach, while students have grades and fees they pay.
You could create separate classes for teachers and students, but that would mean writing the same information (like name and age) twice. That’s definitely not efficient!
Python Inheritance lets you create a main group (class) called SchoolMember that stores all the common information. Then, you can create smaller groups, like Teacher and Student, that inherit those things shared by SchoolMember.
Think of it like a family tree: SchoolMember is the parent class, and Teacher and Student are the child classes that inherit from the parent.
Let’s bring that school information system concept to life with a code example:
class SchoolMember: # Parent (base) class
def __init__(self, name, age, address):
self.name = name
self.age = age
self.address = address
def display_info(self):
print(f"Name: {self.name}, Age: {self.age}, Address: {self.address}")
class Teacher(SchoolMember): # Child (derived) class
def __init__(self, name, age, address, salary, courses):
super().__init__(name, age, address) # Inherit from SchoolMember
self.salary = salary
self.courses = courses
class Student(SchoolMember): # Another child class
def __init__(self, name, age, address, marks, fees):
super().__init__(name, age, address)
self.marks = marks
self.fees = fees
Explanation
- class
SchoolMember
:- This is our base class, defining shared attributes (name, age, address) and a method (display_info) to print them.
- class
Teacher(SchoolMember)
:- This class inherits from SchoolMember, so teachers get the shared attributes and the display_info method automatically.
- super().__init__(…) calls the parent’s constructor to set those shared attributes.
- It adds extra attributes specific to teachers (salary, courses).
- class
Student(SchoolMember)
:- Similar to Teacher, this class inherits from SchoolMember and adds its own specific attributes (marks, fees).
Usage
teacher1 = Teacher("Ms. Smith", 35, "123 Main St", 50000, ["Math", "Science"])
student1 = Student("Alice", 15, "456 Elm St", 90, 1000)
teacher1.display_info() # Output: Name: Ms. Smith, Age: 35, Address: 123 Main St
student1.display_info() # Output: Name: Alice, Age: 15, Address: 456 Elm St
Notice how both teacher1 and student1 can use the display_info method, even though it was defined only in the parent SchoolMember class. This is the power of inheritance!
Benefits of Python Inheritance
- Code Reuse: No need to write the same code over and over again.
- Relationships: Show how different things are related, just like in real life.
- Organization: Keep your code tidy and easier to understand.
- Flexibility: Make changes to your code without breaking everything else.
- Save Time: Build new things faster by using what you already have.
Python Inheritance Best Practices
- Keep It Simple: Start with small, manageable classes and relationships.
- Choose Meaningful Names: Make it clear how classes are related by their names.
- Use “is-a” Relationships: Child classes should be a type of their parent class (e.g., a cat is a animal).
- Don’t Overuse It: Too much inheritance can make code messy and hard to understand.
- Favor Composition: Sometimes, using objects within objects is a better way to reuse code.
When to Avoid Inheritance
- When things aren’t clearly related: If a cat and a car don’t share much in common, don’t force them into a parent-child relationship.
- When the relationship is “has-a” instead of “is-a”: If an object has another object (like a car has an engine), composition might be better.
- When the child class changes the parent’s behavior too much: Inheritance should extend, not completely rewrite.
- When the inheritance chain gets too long: Too many levels can make code confusing.
- When simpler solutions are available: Sometimes, a regular function or a simpler class structure works just fine.
Composition in Python
In the world of object-oriented programming, composition is like building complex structures using simpler blocks. Instead of having one object inherit features from another (like in inheritance), composition means one object contains other objects as its parts.
Example: A Computer
class CPU:
def process_data(self):
print("Processing data...")
class Memory:
def store_data(self, data):
print("Storing data:", data)
class Computer:
def __init__(self):
self.cpu = CPU() # Computer has a CPU
self.memory = Memory() # and memory
def run_program(self, program):
self.cpu.process_data()
self.memory.store_data(program)
Explanation
- class
CPU
and classMemory
: Define classes for simpler components. - class
Computer
: Main class.self.cpu = CPU()
: Creates a CPU object and stores it as part of the computer.self.memory = Memory()
: Does the same for memory.
def run_program(self, program)
: Method simulates running a program.self.cpu.process_data()
: The computer uses its CPU to process data.self.memory.store_data(program)
: The computer uses its memory to store the program.
Key Points
- Composition models “has-a” relationships (e.g., a computer has a CPU).
- It promotes flexibility. You can swap out parts (e.g., upgrade the CPU) without changing the whole design.
- It often leads to simpler, more reusable code than inheritance.
Choosing Between Inheritance and Composition in Python
In Python, inheritance and composition are two ways to reuse code, but they work differently.
Inheritance is like making a new blueprint (child class) based on an existing one (parent class). The child inherits features from the parent, but can also add its own unique ones.
Example
class Animal: # Parent class
def __init__(self, name):
self.name = name
def speak(self):
print("Animal sound")
class Dog(Animal): # Child class inherits from Animal
def speak(self):
print("Woof!")
Explanation
- class
Animal
: Defines a basic animal with a name and a generic speak method. - class
Dog(Animal)
: Defines a Dog class that inherits from Animal. - Overridden
speak
method: Dog makes a different sound (“Woof!”) than a generic animal.
Composition is like building something by combining smaller parts. Each part is its own object, and the bigger object uses them to do its job.
Example
class Engine:
def start(self):
print("Engine started")
class Car:
def __init__(self, make, model):
self.make = make
self.model = model
self.engine = Engine() # Car has an Engine object as a part
def start(self):
self.engine.start()
Explanation
- class
Engine
: Defines a separate class for the car’s engine. - class
Car
: Defines a Car class. self.engine = Engine()
: Creates an Engine object within the Car object.def start(self)
: Calls the start method of the engine object.
When to Choose?
- Inheritance (is-a relationship): Use when a child object is a type of the parent (e.g., Dog is an Animal).
- Composition (has-a relationship): Use when an object contains other objects (e.g., Car has an Engine).
Think carefully about the relationship between your objects. Python inheritance is powerful, but overuse can make code harder to understand. Composition can often be a simpler, more flexible option.
Conclusion
Python inheritance is powerful that enables code reuse and promotes a structured programming approach. By creating a hierarchy of classes, where child classes inherit attributes and methods from their parents, you can build complex systems without repeating code. This leads to more efficient, maintainable, and elegant code solutions. Understanding inheritance, including single, multiple, and multilevel variations, empowers you to create flexible and scalable applications.