2.1.8OOP Fundamentals

Inheritance — single inheritance, method resolution order (MRO)

2,106 words10 min readdifficulty · medium2 backlinks

WHY does inheritance exist?

The key relationship is is-a (not has-a).

  • A Car is-a Vehicle → inheritance.
  • A Car has-a Engine → 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 Dog
class Puppy(Dog):
    def speak(self):
        base = super().speak()        # 'Woof' from Dog
        return base + " (tiny)"       # extend it

HOW does Python find a method? — The MRO

For single inheritance the MRO is literally the chain: MRO(C)=[C,  Parent(C),  Parent(Parent(C)),  ,  object]\text{MRO}(C) = [\,C,\; \text{Parent}(C),\; \text{Parent}(\text{Parent}(C)),\; \dots,\; \texttt{object}\,]

object is always last — it's the root of every Python class.

Figure — Inheritance — single inheritance, method resolution order (MRO)

Deriving the lookup algorithm from scratch

When you write obj.speak(), Python conceptually does:

  1. Let T = type(obj).
  2. Compute order = T.__mro__ (the precomputed list).
  3. For each class K in order in order:
    • if 'speak' is in K.__dict__ → use K.__dict__['speak'], stop.
  4. 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

Single inheritance means a class derives from how many direct parents?
Exactly one.
What does MRO stand for and what is it?
Method Resolution Order — the ordered list of classes Python searches to resolve a method/attribute.
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 ___?
The first class in the MRO that defines that name in its __dict__.
What does super() mean operationally?
Start attribute lookup at the class immediately AFTER the current one in the MRO.
Inside an inherited method, self.x() resolves using which class?
type(self) — the object's dynamic type (polymorphism).
Override vs extend a method?
Override = replace entirely; Extend = call super().method() then add behavior.
If a child defines __init__, does the parent's run automatically?
No — you must call super().__init__(...) explicitly.
is-a vs has-a guideline?
is-a → use inheritance; has-a → use composition (a field).
Why prefer composition when relationship isn't is-a?
Inheritance forces the parent's whole interface/contract onto the child, causing tight coupling and leaking unwanted methods.

Concept Map

lets

reuses & specializes

requires

else use

simplest form

exactly one parent

replaces method

calls parent then adds

uses

walks

first match wins

MRO is parent chain

Inheritance

Parent class

Child class

is-a relationship

Composition

Single inheritance

Override method

Extend via super

Method Resolution Order

Method lookup

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.

Go deeper — visual, from zero

Test yourself — OOP Fundamentals

Connections