What is it? #
Inheritance lets a class start from another class and add or change parts of it. The new class gets everything the parent has without repeating the code.
You use it when there is a genuine "is a" relationship. A SavingsAccount is a kind of Account. A PdfReport is a kind of Report.
Overriding means redefining a parent method in the child. super() calls the parent version, which is how you extend behaviour instead of replacing it entirely.
Inheritance is easy to overuse. If you find yourself inheriting just to reuse one helper method, or overriding most of the parent, the relationship is not really "is a" — reach for composition instead.
Think of it like this #
Think of job roles. Every employee gets an ID card and can log in. A manager is an employee who can also approve leave. You do not rewrite the ID card system for managers; you start from employee and add the extra ability.
Where it goes wrong: a contractor is not a kind of manager just because both can approve something. Forcing that relationship makes an organisation chart nobody can read.
Simple example #
A payments system supports several methods. All of them record an amount and produce a receipt, but each charges differently and some add a processing fee.
Code #
class Payment:
def __init__(self, amount, reference):
self.amount = amount
self.reference = reference
def fee(self):
return 0.0
def total(self):
return round(self.amount + self.fee(), 2)
def receipt(self):
return f"{self.__class__.__name__}: {self.total()} ({self.reference})"
class CardPayment(Payment):
def __init__(self, amount, reference, last4):
super().__init__(amount, reference) # let the parent set up first
self.last4 = last4
def fee(self): # override
return self.amount * 0.02
def receipt(self):
base = super().receipt() # extend, do not replace
return f"{base} card ****{self.last4}"
class UpiPayment(Payment):
pass # no fee, no changes needed
card = CardPayment(1000, "ORD-1", "4321")
upi = UpiPayment(1000, "ORD-2")
print(card.total(), upi.total()) # 1020.0 1000
print(card.receipt()) # CardPayment: 1020.0 (ORD-1) card ****4321
print(isinstance(card, Payment)) # True
How it works #
class CardPayment(Payment): means "start from Payment". CardPayment immediately has total and receipt without writing them.
super().__init__(amount, reference) runs the parent's initialiser so the shared attributes get set, then the child adds last4. Skipping this call is a common bug — the object ends up half-initialised.
fee is overridden. Notice that total in the parent calls self.fee(), and because self is a CardPayment, Python uses the child's version. That is dynamic dispatch, and it is what makes the parent's total work correctly for every subclass without knowing about them.
receipt shows extending rather than replacing: call super().receipt() for the shared part, then add what is specific.
UpiPayment overrides nothing. It exists as a distinct type so the rest of the system can distinguish it, and it inherits the zero fee.
isinstance(card, Payment) is true, which is the practical meaning of inheritance: a CardPayment can be used anywhere a Payment is expected. That promise is what the Liskov Substitution Principle formalises in the SOLID track.
Real-world use #
Framework code is full of inheritance: you subclass a base view, a base model, a base test case, and override the two methods you care about. This works because the base class was designed as an extension point.
In your own code, the sweet spot is a small, stable base with a handful of subclasses that differ in well-defined ways — payment types, export formats, notification channels.
The failure mode is a deep hierarchy where behaviour is scattered across five levels and you cannot tell which class a method actually comes from. When that happens, teams usually flatten it and pass behaviour in as objects instead, which is composition. The design patterns track covers Strategy, which is the usual replacement.
Common mistakes #
- Forgetting
super().__init__(...), leaving the object partly set up. - Inheriting to reuse one helper method. Import a function or compose instead.
- Overriding a method with different behaviour that breaks the parent’s promise, so code written against the parent fails.
- Building hierarchies more than two or three levels deep.
- Using inheritance when the relationship is "has a" rather than "is a" — a Car has an Engine, it is not an Engine.
Practice #
Create a Notification base class with send() and a summary() method. Add EmailNotification and SmsNotification subclasses that override send(), with the SMS one also enforcing a 160-character limit. Use super() in at least one override, and print a summary for each.