The Son’s Shop Has Everything, and a Little More
Ramesh Agarwal has run a sweet shop in Jaipur for thirty years. His shop has an owner, a city, a list of products and a way to sell them. His son Nikhil opens a new shop. It does everything the father’s shop does, with the same sweets and the same billing, plus one new thing: it takes online orders and delivers them.
Nikhil does not start from zero. He inherits everything that already works and adds only what is new. Python classes can do exactly the same. This is called inheritance: a new class (the child or subclass) gets all the attributes and methods of an existing class (the parent or base class) and can add or change some of them.
In this lesson you will build the family shop with inheritance, model bikes, cars and buses that charge different fares, and then learn special methods: methods with double underscores that let your objects work with print(), len(), ==, sorted() and even +. You should be comfortable with the Classes and Objects lesson before you start.
Inheritance and super()
To inherit, put the parent class name in brackets: class OnlineShop(Shop):. If the child needs its own __init__, it should first call super().__init__(...). super() means “the parent class”, so this line lets Shop set up its own attributes before the child adds new ones.
class Shop:
def __init__(self, owner, city):
self.owner = owner
self.city = city
self.sales = 0
def sell(self, amount):
self.sales += amount
print(f"{self.owner}: sold goods worth Rs {amount}")
class OnlineShop(Shop):
def __init__(self, owner, city, website):
super().__init__(owner, city) # let Shop set up owner, city, sales
self.website = website
self.orders = []
def take_online_order(self, customer, amount):
self.orders.append(customer)
self.sell(amount) # a method inherited from Shop
father = Shop("Ramesh Agarwal", "Jaipur")
son = OnlineShop("Nikhil Agarwal", "Jaipur", "agarwal-sweets.example.com")
father.sell(800)
son.sell(500) # works: inherited
son.take_online_order("Pooja in Delhi", 1200) # only OnlineShop has this
print(son.city, son.sales, son.orders) # Jaipur 1700 ['Pooja in Delhi']
son.sell(500) works even though OnlineShop never defines sell. When Python cannot find a method in the child, it looks in the parent. The father’s shop, however, has no take_online_order: inheritance goes from parent to child, never the other way.
Overriding Methods
Sometimes the child should do something differently. If the child defines a method with the same name as the parent, the child’s version wins. This is called overriding. Inside the new version you can still call the parent’s version with super().method_name(), so you add to it instead of rewriting it.
class Shop:
def __init__(self, owner):
self.owner = owner
def timings(self):
return "Open 9 AM to 9 PM"
def describe(self):
return f"{self.owner}'s shop. {self.timings()}."
class OnlineShop(Shop):
def timings(self): # override: replace the parent's version
return "Orders accepted 24 hours a day"
def describe(self): # extend: reuse the parent's version
basic = super().describe()
return basic + " Free delivery above Rs 499."
print(Shop("Ramesh").describe())
# Ramesh's shop. Open 9 AM to 9 PM.
print(OnlineShop("Nikhil").describe())
# Nikhil's shop. Orders accepted 24 hours a day. Free delivery above Rs 499.
Look carefully: describe() in Shop calls self.timings(). For an OnlineShop, self is the online shop, so Python uses the child’s timings(). The parent’s code automatically picks up the child’s changes.
One Parent, Many Children: Vehicles and Fares
A ride app offers bikes, cars and buses. They all have a number plate and a fare, but each charges a different amount per kilometre. Put the shared code in a Vehicle class and let each child change only what is different. Here the children change just a class attribute, and Bus also overrides fare() to add a minimum ticket.
class Vehicle:
fare_per_km = 0
def __init__(self, number):
self.number = number
def fare(self, km):
return km * self.fare_per_km
def kind(self):
return type(self).__name__
class Bike(Vehicle):
fare_per_km = 8
class Car(Vehicle):
fare_per_km = 14
class Bus(Vehicle):
fare_per_km = 2
def fare(self, km):
return max(10, super().fare(km)) # a minimum ticket of Rs 10
trip_km = 4
for vehicle in [Bike("RJ14 AB 1234"), Car("RJ14 CD 5678"), Bus("RJ14 PA 0042")]:
print(f"{vehicle.kind():<5} {vehicle.number}: Rs {vehicle.fare(trip_km)}")
ride = Car("RJ14 EF 9999")
print(isinstance(ride, Car)) # True
print(isinstance(ride, Vehicle)) # True: a Car is a Vehicle
print(isinstance(ride, Bus)) # False
print(issubclass(Bus, Vehicle)) # True
The loop calls vehicle.fare(trip_km) without asking what kind of vehicle it is. Each object answers in its own way. This idea, one method name with different behaviour, is called polymorphism.
isinstance(obj, Class) checks whether an object was made from a class or any of its children. A Car is also a Vehicle. issubclass() asks the same question about classes.
Special Methods
Special methods (also called dunder methods, from “double underscore”) are methods that Python calls for you when you use built-in operations. You never call __len__() yourself; you write len(cart) and Python calls it.
| You write | Python calls |
|---|---|
print(p), str(p), f-strings | p.__str__() |
repr(p), or p inside a printed list | p.__repr__() |
len(cart) | cart.__len__() |
a == b | a.__eq__(b) |
a < b, sorted(), min() | a.__lt__(b) |
a + b | a.__add__(b) |
__str__ and __repr__
__str__ is the friendly text for users. __repr__ is the exact text for developers, ideally looking like the code that would create the object. When you print a list, Python uses __repr__ for each item, so it is worth writing both. Inside the f-string, !r adds quotes around the name.
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __str__(self):
return f"{self.name} (Rs {self.price})"
def __repr__(self):
return f"Product({self.name!r}, {self.price})"
kaju = Product("Kaju Katli 500 g", 450)
print(kaju) # Kaju Katli 500 g (Rs 450) uses __str__
print([kaju]) # [Product('Kaju Katli 500 g', 450)] lists use __repr__
print(repr(kaju)) # Product('Kaju Katli 500 g', 450)
__len__ and __eq__
By default, == on your objects is True only when both names point to the very same object. Usually you want “same name and same price means equal”. Write __eq__ for that. Returning NotImplemented for other types tells Python “I do not know how to compare these”, and Python then simply answers False.
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __eq__(self, other):
if not isinstance(other, Product):
return NotImplemented
return self.name == other.name and self.price == other.price
class Cart:
def __init__(self):
self.items = []
def add(self, product):
self.items.append(product)
def __len__(self):
return len(self.items)
ladoo = Product("Besan Ladoo", 300)
same_ladoo = Product("Besan Ladoo", 300)
print(ladoo == same_ladoo) # True: same name and price
print(ladoo is same_ladoo) # False: still two separate objects
cart = Cart()
cart.add(ladoo)
cart.add(Product("Soan Papdi", 180))
print(len(cart)) # 2
print(ladoo in cart.items) # True: "in" uses __eq__
__lt__ for Sorting by Price
sorted() needs to know whether one item is “less than” another. Give your class __lt__ (less than), and sorted(), min() and max() all work.
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __lt__(self, other):
return self.price < other.price
def __repr__(self):
return f"{self.name}: Rs {self.price}"
menu = [
Product("Rasgulla tin", 260),
Product("Kaju Katli 500 g", 450),
Product("Namkeen mix", 120),
Product("Gift hamper", 999),
]
print(sorted(menu))
# [Namkeen mix: Rs 120, Rasgulla tin: Rs 260, Kaju Katli 500 g: Rs 450, Gift hamper: Rs 999]
print(min(menu)) # Namkeen mix: Rs 120
print(sorted(menu, reverse=True)[0]) # Gift hamper: Rs 999
__add__ for Combining Carts
__add__ decides what + means. Here, adding two carts makes a new cart with the quantities combined, and the original carts stay as they were. Use __add__ only when + has an obvious meaning, as it does for carts or money.
class Cart:
def __init__(self, items=None):
self.items = dict(items or {})
def __add__(self, other):
combined = Cart(self.items)
for name, qty in other.items.items():
combined.items[name] = combined.items.get(name, 0) + qty
return combined
def __str__(self):
return ", ".join(f"{name} x{qty}" for name, qty in self.items.items())
mother_cart = Cart({"Kaju Katli": 1, "Diya set": 2})
sister_cart = Cart({"Diya set": 1, "Rangoli colours": 3})
family_cart = mother_cart + sister_cart
print(family_cart) # Kaju Katli x1, Diya set x3, Rangoli colours x3
print(mother_cart) # Kaju Katli x1, Diya set x2 (unchanged)
Composition or Inheritance?
Inheritance is not the only way to reuse code. With composition, one object simply holds another object as an attribute. A good test is to say the sentence out loud. “An online shop is a shop” sounds right, so inheritance fits. “An online shop is a delivery partner” sounds wrong; “an online shop has a delivery partner” sounds right, so use composition. Most experienced programmers prefer composition when they are unsure, because it keeps classes small and independent. Avoid deep chains of inheritance; one or two levels are usually enough.
Real-Life Example: Agarwal Sweets Goes Online
Here is Nikhil’s shop with everything together. Product uses __lt__ and __str__. Shop uses __len__ and sorts its menu by price. OnlineShop inherits from Shop, overrides bill() to add a delivery charge for small orders, and has a DeliveryPartner through composition.
class Product:
def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock
def __lt__(self, other):
return self.price < other.price
def __str__(self):
return f"{self.name:<18} Rs {self.price:>4} ({self.stock} left)"
class DeliveryPartner: # composition: a separate helper class
def __init__(self, name, charge):
self.name = name
self.charge = charge
class Shop:
def __init__(self, name):
self.name = name
self.products = []
def add_product(self, product):
self.products.append(product)
def __len__(self):
return len(self.products)
def bill(self, product_name, qty):
for p in self.products:
if p.name == product_name:
if qty > p.stock:
raise ValueError(f"Only {p.stock} {p.name} left")
p.stock -= qty
return p.price * qty
raise ValueError(f"{product_name} not found")
def show_menu(self):
print(f"--- {self.name} ({len(self)} items, cheapest first) ---")
for p in sorted(self.products):
print(p)
class OnlineShop(Shop): # inheritance: an OnlineShop IS a Shop
def __init__(self, name, partner):
super().__init__(name)
self.partner = partner # composition: an OnlineShop HAS a partner
def bill(self, product_name, qty):
amount = super().bill(product_name, qty)
if amount < 500:
amount += self.partner.charge
return amount
shop = OnlineShop("Agarwal Sweets Online", DeliveryPartner("City Riders", 40))
shop.add_product(Product("Kaju Katli 500 g", 450, 10))
shop.add_product(Product("Namkeen mix", 120, 25))
shop.add_product(Product("Gift hamper", 999, 3))
shop.show_menu()
print("Bill for 2 namkeen:", shop.bill("Namkeen mix", 2)) # 240 + 40 delivery = 280
print("Bill for 1 hamper:", shop.bill("Gift hamper", 1)) # 999, free delivery
try:
shop.bill("Gift hamper", 5)
except ValueError as error:
print("Sorry:", error) # Sorry: Only 2 Gift hamper left
Try these changes:
- Change the delivery partner’s charge to Rs 25 and check the namkeen bill.
- Add a
__repr__toProductand printshop.products. - Create a second
OnlineShopwith a different delivery partner and compare bills.
Common Mistakes
- Forgetting
super().__init__(). When the child has its own__init__, the parent’s__init__does not run by itself, so the parent’s attributes are never created:
class Shop:
def __init__(self, owner):
self.owner = owner
class OnlineShop(Shop):
def __init__(self, owner, website):
self.website = website # forgot super().__init__(owner)
son = OnlineShop("Nikhil", "agarwal-sweets.example.com")
print(son.owner) # ERROR: AttributeError
Fix: call super().__init__(...) first in the child’s __init__:
class Shop:
def __init__(self, owner):
self.owner = owner
class OnlineShop(Shop):
def __init__(self, owner, website):
super().__init__(owner)
self.website = website
son = OnlineShop("Nikhil", "agarwal-sweets.example.com")
print(son.owner) # Nikhil
- Passing
selftosuper().super().__init__(self, owner)sends one argument too many and raises aTypeError.super()already knows the object, so writesuper().__init__(owner). - Returning the wrong type from a special method.
__str__and__repr__must return a string,__len__must return a whole number of 0 or more, and__eq__and__lt__should returnTrueorFalse. Otherwise you get aTypeError. - Using inheritance for a “has a” relationship.
class Shop(DeliveryPartner)gives the shop methods it should not have. Fix: store the partner as an attribute (composition).
Practice Task: Employees and Managers
A small company pays employees a monthly salary. Managers get a salary plus a bonus. Complete the starter code:
- Create
Manageras a child ofEmployeewith an extrabonus, usingsuper().__init__. - Override
monthly_pay()inManagerto return salary plus bonus, usingsuper(). - Add
__lt__toEmployeeso thatsorted(staff)sorts by monthly pay. - Add
__eq__so two employees with the same name and pay are equal.
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def monthly_pay(self):
return self.salary
def __repr__(self):
return f"{self.name}: Rs {self.monthly_pay()}"
# TODO 1: create class Manager(Employee) with an extra "bonus" attribute.
# Use super().__init__ for name and salary.
# TODO 2: override monthly_pay() so it returns salary + bonus (use super()).
# TODO 3: add __lt__ to Employee so employees sort by monthly_pay().
# TODO 4: add __eq__ to Employee: equal when name and monthly_pay() match.
staff = [
Employee("Ravi", 18000),
Employee("Sneha", 22000),
# Manager("Deepa", 30000, 5000), # remove the # when Manager exists
]
print(sorted(staff, key=lambda e: e.monthly_pay())) # later: print(sorted(staff))
Show one possible answer (try it yourself first!)
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def monthly_pay(self):
return self.salary
def __lt__(self, other):
return self.monthly_pay() < other.monthly_pay()
def __eq__(self, other):
if not isinstance(other, Employee):
return NotImplemented
return self.name == other.name and self.monthly_pay() == other.monthly_pay()
def __repr__(self):
return f"{self.name}: Rs {self.monthly_pay()}"
class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary)
self.bonus = bonus
def monthly_pay(self):
return super().monthly_pay() + self.bonus
staff = [
Manager("Deepa", 30000, 5000),
Employee("Ravi", 18000),
Employee("Sneha", 22000),
]
print(sorted(staff)) # [Ravi: Rs 18000, Sneha: Rs 22000, Deepa: Rs 35000]
print(isinstance(staff[0], Employee)) # True
print(Employee("Ravi", 18000) == Employee("Ravi", 18000)) # True
Key Takeaways
class Child(Parent):gives the child every attribute and method of the parent.- Call
super().__init__(...)in the child’s__init__, and usesuper().method()to extend a parent method instead of copying it. - Overriding means defining a method with the same name in the child;
isinstance()also returnsTruefor parent classes. - Special methods connect your objects to Python:
__str__,__repr__,__len__,__eq__,__lt__and__add__. - Use inheritance for “is a” and composition for “has a”.