Inheritance — single inheritance, method resolution order (MRO)
WHY does inheritance exist?
The key relationship is is-a (not has-a).
- A
Caris-aVehicle→ inheritance. - A
Carhas-aEngine→ composition (a field, not a parent).
WHAT is single inheritance?
class Animal: # parent / base / superclass
def __init__(self, name):
self.name = name
def speak(self):
return "..."
class Dog(Animal): # child / derived / subclass
def speak(self): # OVERRIDE
return "Woof"
d = Dog("Rex")
print(d.name) # 'Rex' -> inherited __init__ from Animal
print(d.speak()) # 'Woof' -> overridden in Dogclass Puppy(Dog):
def speak(self):
base = super().speak() # 'Woof' from Dog
return base + " (tiny)" # extend itHOW does Python find a method? — The MRO
For single inheritance the MRO is literally the chain:
object is always last — it's the root of every Python class.

Deriving the lookup algorithm from scratch
When you write obj.speak(), Python conceptually does:
- Let
T = type(obj). - Compute
order = T.__mro__(the precomputed list). - For each class
Kinorderin order:- if
'speak'is inK.__dict__→ useK.__dict__['speak'], stop.
- if
- If none found →
AttributeError.
That's it — no magic. super() simply means "start the same search, but at the position after the current class in the MRO."
Worked Example 1 — basic chain
class A:
def who(self): return "A"
class B(A):
pass
class C(B):
def who(self): return "C"
c = C()
print(C.__mro__) # (C, B, A, object)
print(c.who()) # 'C'Why (C, B, A, object)? Single inheritance → just the parent chain, then object. Why this step? C is most specific so it's first; object roots everything so it's last.
Why c.who() == 'C'? Search order is C, B, A, object. C defines who → first match → stop. Why this step? B has no who, but we never reach it because C already matched.
Worked Example 2 — inheritance fills the gap
b = B()
print(b.who()) # 'A'Why 'A'? B.__mro__ = (B, A, object). B has no who, A does. Why this step? Lookup keeps walking until a class defines the name; B is skipped, A wins.
Worked Example 3 — super() to extend, not replace
class Account:
def __init__(self, balance):
self.balance = balance
def describe(self):
return f"balance={self.balance}"
class Savings(Account):
def __init__(self, balance, rate):
super().__init__(balance) # run parent's __init__
self.rate = rate
def describe(self):
return super().describe() + f", rate={self.rate}"
s = Savings(100, 0.05)
print(s.describe()) # 'balance=100, rate=0.05'Why call super().__init__? Because Account.__init__ sets self.balance. If we skip it, self.balance never exists. Why this step? The child is responsible for initializing the parent's part of the object.
Why does super().describe() give the Account version? super() inside Savings starts lookup at the class after Savings in the MRO (Savings, Account, object) → that's Account. Why this step? super() is "next in MRO", not "literal parent" — for single inheritance they coincide.
Worked Example 4 — Forecast-then-Verify
The 80/20 — what actually matters
Active Recall
Recall What is the MRO of
class C(B), class B(A)?
(C, B, A, object) — the single-inheritance chain plus object.
Recall Inside an inherited method,
self.foo() resolves against which class?
Against type(self).__mro__ — the dynamic type of the object, not the class where the method's code lives.
Recall Why must you call
super().__init__() in a child's __init__?
Defining __init__ in the child replaces the parent's; the parent's initialization (e.g. setting fields) won't run unless you explicitly call it.
Recall is-a vs has-a?
is-a → inheritance (Car is-a Vehicle). has-a → composition (Car has-a Engine, as a field).
Recall 🧒 Feynman: explain to a 12-year-old
Think of classes like recipe cards. A "Dog" recipe says "I'm just like the 'Animal' recipe, but I bark instead of going '...'." When you ask the dog to do something, you flip through the cards starting with the most specific one (Dog), then Animal, then the super-general "object" card, and you do whatever the first card that mentions that action tells you. That ordered stack of cards is the MRO. super() just means "skip my own card and check the next one down."
Connections
- OOP Fundamentals
- Polymorphism and Method Overriding
- super() and cooperative multiple inheritance
- Composition over Inheritance
- C3 Linearization (the algorithm behind multiple-inheritance MRO)
- Encapsulation and Attribute Lookup
- object — the root class
Single inheritance means a class derives from how many direct parents?
What does MRO stand for and what is it?
For class C(B) and class B(A), what is C.__mro__?
(C, B, A, object).Which class is always last in any MRO?
object, the root of all classes.The lookup rule: a name resolves to ___?
__dict__.What does super() mean operationally?
Inside an inherited method, self.x() resolves using which class?
type(self) — the object's dynamic type (polymorphism).Override vs extend a method?
super().method() then add behavior.If a child defines __init__, does the parent's run automatically?
super().__init__(...) explicitly.is-a vs has-a guideline?
Why prefer composition when relationship isn't is-a?
Concept Map
Hinglish (regional understanding)
Intuition Hinglish mein samjho
Inheritance ka matlab simple hai: ek child class apne parent class ka saara code reuse kar leti hai, aur sirf jo extra ya alag chahiye wahi add/change karti hai. Rule yaad rakho — is-a ho to inheritance use karo (Dog is-a Animal), aur has-a ho to composition (Car ke andar Engine ek field hai). Sirf code reuse ke liye blindly inherit mat karna, warna parent ki saari unwanted methods bhi child me leak ho jaati hain.
Ab MRO (Method Resolution Order) — jab tum obj.method() call karte ho, Python ko pata hona chahiye method kahaan se uthana hai. Single inheritance me MRO bas ek seedhi chain hoti hai: Child → Parent → GrandParent → ... → object. Python is list me upar se neeche dhoondhta hai aur jo pehli class us name ko define karti hai, wahi jeet jaati hai. Bas — koi magic nahi. C.__mro__ likh ke ye list dekh sakte ho.
super() ka matlab hai "MRO me mere just baad wali class se dhoondhna shuru karo". Isiliye child ke __init__ me super().__init__() likhna zaroori hai — warna parent ka initialization (jaise fields set karna) chalega hi nahi, kyunki child ne __init__ ko replace kar diya. Ek important polymorphism point: inherited method ke andar bhi self.tag() ka lookup type(self) ke hisaab se hota hai, na ki us class ke hisaab se jahaan code likha hai. Matlab agar object Dog hai, to overridden Dog wali method hi call hogi.
Exam aur real coding dono me yehi 20% sab kuch sambhal leta hai: is-a vs has-a, chain-wala MRO, "first match wins", super() for extend, aur type(self)-based dispatch. Inhe ek baar haath se trace kar lo, OOP clear ho jaayega.