Inheritance: "is-a" relationships
Inheritance lets a new class reuse an existing one. The new class (the child or subclass) gets every attribute and method of the parent (base class), and can add or change some.
Use inheritance when the child really *is a* kind of the parent: a Manager is an Employee, a Car is a Vehicle. If the relationship is "has a" (a Car *has an* Engine), store an object as an attribute instead.
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def pay(self):
return self.salary
class Manager(Employee): # Manager inherits from Employee
pass
m = Manager("Asha", 50000)
print(m.name, m.pay()) # Asha 50000 -- inherited
Child classes with super()
class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary) # let the parent set name and salary
self.bonus = bonus # then add what is new
def pay(self): # override the parent's method
return super().pay() + self.bonus
m = Manager("Asha", 50000, 10000)
print(m.pay()) # 60000
super()gives you the parent class, so you can call its version of a method.- Overriding means writing a method in the child with the same name as one in the parent. The child's version wins.
Which method runs?
When you call m.pay(), Python looks for pay in Manager first. If it is not there, it looks in Employee, then in object (the parent of every class). The first one found runs. This search order is called the method resolution order, and Manager.__mro__ shows it.
isinstance(m, Employee) is True: a Manager counts as an Employee.
Polymorphism: one interface, many forms
Polymorphism means different classes can be used the same way, because they share method names.
class Auto:
def fare(self, km):
return 30 + km * 15
class Cab:
def fare(self, km):
return 50 + km * 20
class Bike:
def fare(self, km):
return 20 + km * 8
for ride in [Auto(), Cab(), Bike()]:
print(type(ride).__name__, ride.fare(5))
The loop does not care which class each ride is; it only needs a fare() method. Adding a new vehicle needs no change to the loop.
Protecting data: _private names and @property
Python has no truly private attributes. By convention, a name starting with _ means "internal: do not touch from outside".
@property lets you check values while keeping simple obj.attribute syntax:
class Account:
def __init__(self, balance):
self._balance = balance
@property
def balance(self): # read: acc.balance
return self._balance
@balance.setter
def balance(self, value): # write: acc.balance = value
if value < 0:
raise ValueError("balance cannot be negative")
self._balance = value
acc = Account(100)
acc.balance = 50 # goes through the setter's check
acc.balance = -5 # ValueError
Dunder (magic) methods
Methods with double underscores on both sides (dunder methods) let your objects work with Python's operators and built-in functions.
| You write | Python calls |
|---|---|
print(a) | a.__str__() |
a + b | a.__add__(b) |
a == b | a.__eq__(b) |
a < b | a.__lt__(b) |
len(a) | a.__len__() |
class Money:
def __init__(self, rupees):
self.rupees = rupees
def __add__(self, other):
return Money(self.rupees + other.rupees)
def __lt__(self, other):
return self.rupees < other.rupees
def __str__(self):
return f"Rs {self.rupees}"
print(Money(100) + Money(250)) # Rs 350
print(Money(100) < Money(250)) # True
Defining __lt__ also lets sorted() and min() work on a list of Money objects.
Abstract base classes
An abstract class is a parent that only describes which methods its children must have. You cannot create objects from it directly.
from abc import ABC, abstractmethod
class Payment(ABC):
@abstractmethod
def pay(self, amount):
...
class UPI(Payment):
def pay(self, amount):
print(f"Paid Rs {amount} by UPI")
UPI().pay(200)
Payment() # TypeError: can't instantiate abstract class
A child that forgets to write pay() also cannot be created, so the mistake is caught early.
Common mistakes
- Forgetting
super().__init__(...)in the child, so the parent's attributes are never set. - Using inheritance for a "has-a" relationship.
- Overriding a method with a misspelt name, so the parent's version still runs.
- Writing
__add__that changesselfinstead of returning a new object.
Key points
- A child class inherits from its parent and can override methods.
super()calls the parent's version of a method.- Polymorphism: different classes, same method names, used the same way.
- Dunder methods connect your classes to
+,==,<,len()andprint().
Practice questions
1. What does super().__init__(name) do inside a child's __init__?
It runs the parent's __init__, so the attributes the parent sets up are created on the child object too.
2. Dog inherits from Animal. Both have speak(). Which runs for Dog().speak()?
Dog's version, because Python looks in the child class first.
3. Write Circle and Square classes with an area() method, and print both areas in one loop.
class Circle:
def __init__(self, r): self.r = r
def area(self): return 3.14 * self.r ** 2
class Square:
def __init__(self, s): self.s = s
def area(self): return self.s ** 2
for shape in [Circle(2), Square(3)]:
print(shape.area())4. Which dunder method makes len(obj) work?
__len__, which must return a whole number.
5. Why can you not create an object from an abstract class?
It has abstract methods with no real code. Python refuses, so that only complete child classes are used.