☰ Learn Python Tutorial Menu
Inheritance and Dunder Methods
Written by CSA mentors · Updated 26 Sept 2026 · 5 min read
Give this lesson an hour and your classes will behave like built-in types. They'll print nicely, compare with ==, sort with sorted(), add up with sum(). Two ideas get you there. Inheritance lets a child class reuse a parent's code and change only what differs, so a savings account and a current account can both be accounts without copying code. Dunder methods (double-underscore methods like __str__) are how Python connects operators and built-ins to your objects.
Inheritance basics
class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("insufficient funds")
self.balance -= amount
def __str__(self):
return f"{type(self).__name__} of {self.owner}: Rs {self.balance:,}"
class SavingsAccount(Account):
def __init__(self, owner, balance=0, rate=0.08):
super().__init__(owner, balance) # let the parent set owner and balance
self.rate = rate
def add_profit(self):
self.balance += round(self.balance * self.rate / 12)
class CurrentAccount(Account):
def __init__(self, owner, balance=0, overdraft_limit=50000):
super().__init__(owner, balance)
self.overdraft_limit = overdraft_limit
def withdraw(self, amount): # override the parent's version
if amount > self.balance + self.overdraft_limit:
raise ValueError("over overdraft limit")
self.balance -= amount
s = SavingsAccount("Sana", 120000)
s.add_profit()
c = CurrentAccount("Zara Textiles", 10000)
c.withdraw(40000) # allowed: goes to -30000
print(s) # SavingsAccount of Sana: Rs 120,800
print(c) # CurrentAccount of Zara Textiles: Rs -30,000
What to notice in there.
class SavingsAccount(Account):names the parent in parentheses. The child gets every parent method for free, sos.deposit()works without being written again.super().__init__(...)calls the parent's constructor so shared setup lives in one place. Forgetting this line is the most common inheritance bug we see, and the symptom is anAttributeErroraboutownerorbalance.- Defining a method with the same name in the child overrides the parent's.
CurrentAccount.withdrawreplacesAccount.withdraw, whileSavingsAccountstill uses the original. type(self).__name__in the parent's__str__prints the actual class name, so one__str__serves all the children.
isinstance and polymorphism
accounts = [s, c, Account("Cash", 5000)]
for a in accounts:
a.deposit(1000) # same call, each does the right thing
print(a, "| savings?", isinstance(a, SavingsAccount))
Calling the same method on different types and having each respond in its own way is polymorphism. A report function can accept any account without caring which kind it is. isinstance(obj, Class) is true for the class and all its children. Use it rarely. If you find yourself checking types inside a loop, the design usually wants another method on the parent instead.
Dunder methods
Python maps operators and built-ins to specially named methods. Implement them and your class plugs straight into the language.
| You write | Python calls | Typical use |
|---|---|---|
print(x), str(x) | x.__str__() | Human-readable text |
x in a list, debugger | x.__repr__() | Developer text, ideally valid code |
x == y | x.__eq__(y) | Compare by value, not identity |
x < y, sorted() | x.__lt__(y) | Ordering |
x + y | x.__add__(y) | Combining, e.g. Money |
len(x) | x.__len__() | Containers |
x[key] | x.__getitem__(key) | Indexing |
for item in x | x.__iter__() | Looping |
with x: | __enter__, __exit__ | Cleanup, like files |
A Money class
class Money:
def __init__(self, paisa):
self.paisa = int(paisa) # store whole paisa: no float errors
@classmethod
def rupees(cls, amount):
return cls(round(amount * 100))
def __repr__(self):
return f"Money.rupees({self.paisa / 100:.2f})"
def __str__(self):
return f"Rs {self.paisa / 100:,.2f}"
def __add__(self, other):
return Money(self.paisa + other.paisa)
def __eq__(self, other):
return isinstance(other, Money) and self.paisa == other.paisa
def __lt__(self, other):
return self.paisa < other.paisa
a = Money.rupees(1250.75)
b = Money.rupees(0.25)
print(a + b) # Rs 1,251.00
print(a == Money.rupees(1250.75)) # True
print(sorted([a, b, Money.rupees(99)])) # [Money.rupees(0.25), Money.rupees(99.00), Money.rupees(1250.75)]
print(sum([a, b], Money(0))) # Rs 1,251.00
Storing whole paisa as an integer is a deliberate choice, and it's the one we recommend for any money class. It sidesteps the float problems from Lesson 3 entirely. @classmethod receives the class instead of an instance and is the standard way to offer alternative constructors like Money.rupees(). __repr__ is what you see inside lists and error messages, so make it unambiguous. Once __lt__ exists, sorted and min just work.
A container class
class Ledger:
def __init__(self):
self._entries = []
def add(self, description, amount):
self._entries.append((description, amount))
def __len__(self):
return len(self._entries)
def __iter__(self):
return iter(self._entries)
def __getitem__(self, index):
return self._entries[index]
led = Ledger()
led.add("Sales", 45000)
led.add("Rent", -20000)
print(len(led), led[0])
for desc, amt in led:
print(desc, amt)
The leading underscore in _entries is a convention meaning "internal, don't touch from outside". Python doesn't enforce it, but every Python programmer respects it, and reviewers will comment if you reach past it.
Composition over inheritance
Inheritance models "is a". A savings account is an account. When the relationship is "has a", give the class an attribute instead. An Invoice has a Customer, it's not a kind of customer. Deep inheritance trees turn brittle fast, and one or two levels is plenty for analytics code.
Tariffs as classes at K-Electric: A billing prototype has a base
Tariffwithbill(units), and childrenResidentialTariff,CommercialTariffandAgriculturalTariffthat override only the slab logic. The monthly run loops over hundreds of thousands of customers callingcustomer.tariff.bill(units)without a singleifon category. When a new "lifeline" tariff came in, it was a 15-line child class and no other file changed.
Two ideas to keep
First, class Child(Parent): inherits everything, super().__init__() reuses the parent's setup, and redefining a method in the child overrides it while the parent's other methods stay. Second, dunder methods connect your class to print, ==, <, +, len and loops, so always define __repr__, add __eq__ and __lt__ when objects need comparing or sorting, and prefer composition ("has a") over deep inheritance ("is a").
Three to try
- Extend
Accountwith aFixedDepositclass whosewithdrawraises an error before amaturity_dateand works normally after it. - Add
__sub__and__mul__(multiply by a number) toMoney, and makeMoney.rupees(100) * 3 == Money.rupees(300)printTrue. - Give
Ledgerabalance()method and a__str__that lists every entry and the balance, then write aMonthlyLedger(Ledger)child that also stores a month label.
Lesson 19 of 26
Sign in to track your progress and earn learning points for every lesson you finish.
