B3.2.1 Explain and apply the concept of inheritance in OOP to promote code reusability.
• How inheritance enables a hierarchical relationship between parent and child classes
• Extending existing classes, utilizing inheritance to reuse and extend functionalities
• The impact of inheritance on access to parent class members with different access modifiers (private, public, protected, default)
The big idea
Inheritance in object-oriented programming allows one class (child/subclass) to reuse and extend the attributes and methods of another class (parent/superclass).
It creates a hierarchical relationship — much like real-world categories:
Vehicle(parent)Car(child)Motorbike(child)
This hierarchy promotes code reusability, reduces duplication, and makes programs easier to maintain.
1. Hierarchical relationship
In Python, a child class inherits from a parent by specifying the parent in parentheses:
class Vehicle:
def __init__(self, brand):
self.brand = brand
def start(self):
return f"{self.brand} vehicle starting..."
class Car(Vehicle):
def honk(self):
return "Beep beep!"
my_car = Car("Toyota")
print(my_car.start()) # Inherited from Vehicle
print(my_car.honk()) # Defined in Car
Key point:
Carautomatically has access tostart()fromVehicle.- We didn’t have to rewrite
start()inCar.
2. Extending existing classes
A child class can:
- Use all public and protected members from the parent.
- Add new methods and attributes.
- Override existing methods to change behavior.
class ElectricCar(Car):
def __init__(self, brand, battery_capacity):
super().__init__(brand) # Call parent constructor
self.battery_capacity = battery_capacity
# Override start method
def start(self):
return f"{self.brand} electric car silently starting..."
def charge(self):
return f"Charging to {self.battery_capacity} kWh."
tesla = ElectricCar("Tesla", 75)
print(tesla.start()) # Overridden method
print(tesla.honk()) # From Car
print(tesla.charge()) # Unique to ElectricCar
3. Access modifiers in Python
Python does not enforce access modifiers as strictly as Java or C++, but uses naming conventions and name mangling:
| Access Type | Syntax | Meaning |
|---|---|---|
| Public | self.name | Accessible everywhere |
| Protected | self._name | Intended for internal or subclass use only (not enforced) |
| Private | self.__name | Name-mangled to prevent accidental access (still accessible if forced) |
Example:
class Account:
def __init__(self, owner, balance):
self.owner = owner # public
self._account_type = "Savings" # protected
self.__balance = balance # private
def deposit(self, amount):
self.__balance += amount
return f"New balance: {self.__balance}"
def get_balance(self):
return self.__balance
class PremiumAccount(Account):
def upgrade(self):
# Can access public and protected members
self._account_type = "Premium"
# Cannot directly access __balance (private)
# self.__balance = 0 # Would cause AttributeError
return f"Upgraded {self.owner} to {self._account_type}"
acct = PremiumAccount("Alice", 1000)
print(acct.upgrade())
print(acct.get_balance()) # Uses a public method to access private data
Why inheritance matters for reusability
- Without inheritance: Each class must define its own duplicate logic.
- With inheritance: Shared logic lives in the parent; children only add or customize.
- Result: Smaller codebase, fewer bugs, easier updates.
Example:
If you add logging to the start() method in Vehicle, all subclasses automatically get the updated behavior without changing their own code.
Best practices
- Use inheritance for is-a relationships (e.g.,
Caris aVehicle). - Avoid deep inheritance chains — they can be hard to follow.
- Combine inheritance with composition where appropriate.
- Clearly document which methods are safe for subclasses to override.