chalkline

Public teaching whiteboard

Python inheritance & polymorphism — OOP teaching whiteboard

A 2-hour Python OOP whiteboard on inheritance and polymorphism with concrete class examples for oral teaching. · by zlu

Topics: Python whiteboards

Python inheritance & polymorphism — OOP teaching whiteboard
0 · Agenda (0:00)
1 · Warm-up (0:00–0:10)
2 · Override vs extend (0:10–0:40)
3 · Polymorphism (0:40–1:05)
4 · Nominal checks (1:05–1:25)
5 · MRO (1:25–1:45)
6 · Diamond lab (1:45–1:50)
7 · Practice & wrap (1:50–2:00)
Quick reference

py-intro · 4.5 Inheritance & Polymorphism

Live session · 2 hours · Matheion m75 materials

0:00–0:10 Warm-up & goals Where inheritance sits after classes

0:10–0:40 Override vs extend Coffee café · super() · two bugs

0:40–1:05 Polymorphism One call site · many forms · alarms

1:05–1:25 Nominal checks isinstance · type is · issubclass

1:25–1:50 Method Resolution Order (MRO) & diamond Predict · run · __mro__ · cooperative init

1:50–2:00 Practice & wrap Quiz prompts · takeaways

Three questions for today

1. Override or extend?

Does the parent's behaviour still belong in the result?

2. What is polymorphism?

Same call site · different object · different behaviour

3. Where does super() go?

Next in the MRO — not "my literal parent"

Instructor kit • REPL shared screen • This board as spine • m75 quiz at end • Leave diamond for prediction

Student exits with • extend vs override rule • isinstance vs type is • can read Class.__mro__ • keeps calling super() in diamonds

Warm-up — inheritance in one picture

Ask: what do you already get "for free" from a parent class?

Coffee make() → espresso

Cappuccino extend + milk

Mocha override recipe

Live ask "If Cappuccino is a Coffee, what methods does it have before we write anything?"

Bridge from classes • attributes + methods • __init__ sets state • Today: reuse + specialise

Vocabulary

subclass class Child(Parent):

override same name · replace body · no super()

extend same name · call super() · add steps

polymorphism one call · many forms

MRO ordered list Python searches for a method

nominal checks trust declared hierarchy by name

Override vs extend — Harbor Café coffee

One question: does the parent's behaviour still belong in the final result?

class Coffee: def make(self) -> str: return "espresso"

class Cappuccino(Coffee): def make(self) -> str: return f"{super().make()} + steamed milk" # → espresso + steamed milk # EXTEND — keep parent work

class Mocha(Coffee): def make(self) -> str: return "chocolate + espresso + milk" # no super() # OVERRIDE — replace

Two directions of extending

POST-process base = super().make() return base + "…" (Cappuccino)

PRE-process if shots < 1: raise … base = super().make() return f"{base} x{shots}" (DoubleShot)

Two classic bugs (act these out)

Bug 1 — silent override super().make() # discarded! return "quiet coffee" Parent ran · result lost

Bug 2 — accidental extend Meant to replace, but called super() from habit → layered / confusing output

Rule of thumb YES → parent's work belongs → EXTEND with super() NO → parent's work is wrong → OVERRIDE · skip super()

__init__ is usually extend class Gauge(Meter): def __init__(self, label, max_v): super().__init__(label) self.max_value = max_v Forgetting super() = half-init

Live demo (8 min) 1. Type Coffee + both kids 2. print each .make() 3. Break SilentCoffee 4. Fix: base = super().make() 5. Gauge __init__

Polymorphism — one call site, many forms

"Poly-morphism" = many forms. The call looks the same; the object decides.

Key insight sound_off never asks "what kind of alarm?" No if / isinstance ladder. Just d.trigger().

Duck typing Python attribute dispatch cares about behaviour: if it has .trigger(), it runs. Declared type is optional.

Contrast later Nominal checks (isinstance) trust the *name* in the tree. Different tool · different job.

Smell to avoid if isinstance(a, X): … elif isinstance(a, Y): … elif isinstance(a, Z): … → prefer a method / singledispatch

Live ask Add BlinkAlarm with trigger → "flash". Does sound_off change? (No — that's the point.)

Optional: ABC peek from abc import ABC, abstractmethod class Alarm(ABC): @abstractmethod def trigger(self): ... Can't instantiate until subclass implements it. Recognition only today.

Checkpoint Can students explain: "same call, different object, different behaviour" in one sentence?

Nominal polymorphism — isinstance / type / issubclass

Nominal = name-based. These checks trust the declared class hierarchy.

Pick the right tool

isinstance(obj, Cls) "this class OR anything declared under it" Lenient · usual choice

type(obj) is Cls "exactly this class" Rejects subclasses on purpose

issubclass(A, B) About classes, not instances Hierarchy question

Trap: Object vs object object (lowercase) is the root of every class. Object → NameError isinstance("a", object) → True

Substitutability (LSP lite) Because Berry is declared under Fruit, hand a Berry anywhere a Fruit is expected. isinstance is meaningful only if subclasses keep promises.

Live drill (10 min) Predict each line before run. Then flip: invent ExactBerry check that rejects subclasses. When would you want that?

Good uses of isinstance • dispatch on declared family • one special-case fallback • accept Base + children Bad: growing if/elif ladder

Board vote b = Berry() isinstance(b, Fruit)? type(b) is Fruit? Hands up before reveal.

MRO — method resolution order

super() means "next class in the MRO" — not "my literal parent".

Diamond — predict before you run

A

B

C

D

class D(B, C) MRO → (D, B, C, A, object) ping → D B C A After B, super() hops to C — not to A!

Swap parents class D(C, B) MRO → (D, C, B, A, object) output → D C B A Listed order is a contract.

How to inspect Klass.__mro__ Klass.mro() help(Klass)

Cooperative MI rule Every class in a diamond should call super() for chained methods / __init__. Skip once → silent holes.

Teaching move 1. Cover output 2. Students write prediction 3. Run · compare to __mro__ 4. Swap (C,B) · re-predict

Keep diamonds shallow Prefer mixins: small helpers, often no __init__. Deep webs = MRO surprises.

Lab — cooperative __init__ through a diamond

Every class sets one field and calls super().__init__(). Watch the MRO order.

Break it on purpose Comment out super() in B. Re-run. Which fields vanish? Why?

Expected after break C never runs A never runs → no c, no a Silent · no exception That's the danger.

Also try Point3D.dump() extend pattern from m75 examples: data = super().dump() data.update({...}) return data

Time box: 5 min pair work then 2 min share

Practice check + takeaways

Use Matheion quiz m75 (8 items). Attempt before Discuss / reveal.

Q1 subclass __init__ → usually super().__init__(...)

Q2 isinstance(Berry(), Fruit) → True

Q3 override = replace; extend = super() + add

Q4 device.trigger() many ways → polymorphism

Q5 type(Berry()) is Fruit → False (exact)

Q6 D(B,C) MRO → (D, B, C, A, object)

Q7 isinstance is nominal (declared names)

Q8 super().save() then log → extending

Exit tickets (say out loud)

1. One sentence: override vs extend

2. isinstance vs type(...) is — when?

3. What does super() mean in a diamond?

4. Why keep calling super() in __init__?

Homework / self-study • Type diamond · predict · run • Swap D(C,B) · re-predict • __init__ diamond · all fields • Convert one isinstance ladder → polymorphic method Material: m75 concept + examples

Quick reference

Child keeps parent result → EXTEND · call super() · use return value

Child prepares inputs first → EXTEND · validate · then super()

Parent behaviour is wrong → OVERRIDE · leave super() out

This type or under it → isinstance(obj, Cls)

Exactly this class → type(obj) is Cls

Is class under another? → issubclass(A, B)

See dispatch order → Klass.__mro__

Growing isinstance ladder → → method / singledispatch

Auto · Python
# Single inheritance — boring & clear
class A:
    def ping(self):
        print("A")
class B(A):
    def ping(self):
        print("B"); super().ping()
class C(B):
    def ping(self):
        print("C"); super().ping()

C().ping()      # C B A
print(C.__mro__)  # (C, B, A, object)
Auto · Python
class A:
    def __init__(self):
        print("A init"); self.a = "a"
class B(A):
    def __init__(self):
        print("B init"); super().__init__(); self.b = "b"
class C(A):
    def __init__(self):
        print("C init"); super().__init__(); self.c = "c"
class D(B, C):
    def __init__(self):
        print("D init"); super().__init__(); self.d = "d"

d = D()
print(d.__dict__)
# D init → B init → C init → A init
# {'a':'a','c':'c','b':'b','d':'d'}
Auto · Python
class Alarm:
    def trigger(self):
        raise NotImplementedError

class LoudAlarm(Alarm):
    def trigger(self):
        return "BUZZ"

class QuietAlarm(Alarm):
    def trigger(self):
        return "blink"

def sound_off(devices):
    return [d.trigger() for d in devices]

print(sound_off([LoudAlarm(), QuietAlarm()]))
# ['BUZZ', 'blink']
Python
class Fruit: pass
class Berry(Fruit): pass
b = Berry()

isinstance(b, Berry)      # True
isinstance(b, Fruit)      # True  ← subclasses count
type(b) is Berry          # True
type(b) is Fruit          # False ← exact class only
issubclass(Berry, Fruit)  # True

Drag to pan · scroll to zoom · read-only

Board contents

Text extracted from this public whiteboard for search and accessibility.

0 · Agenda (0:00)

py-intro · 4.5 Inheritance & Polymorphism

Live session · 2 hours · Matheion m75 materials

0:00–0:10 Warm-up & goals Where inheritance sits after classes

0:10–0:40 Override vs extend Coffee café · super() · two bugs

0:40–1:05 Polymorphism One call site · many forms · alarms

1:05–1:25 Nominal checks isinstance · type is · issubclass

1:25–1:50 Method Resolution Order (MRO) & diamond Predict · run · __mro__ · cooperative init

1:50–2:00 Practice & wrap Quiz prompts · takeaways

Three questions for today

1. Override or extend?

Does the parent's behaviour still belong in the result?

2. What is polymorphism?

Same call site · different object · different behaviour

3. Where does super() go?

Next in the MRO — not "my literal parent"

Instructor kit • REPL shared screen • This board as spine • m75 quiz at end • Leave diamond for prediction

Student exits with • extend vs override rule • isinstance vs type is • can read Class.__mro__ • keeps calling super() in diamonds

2 · Override vs extend (0:10–0:40)

Override vs extend — Harbor Café coffee

One question: does the parent's behaviour still belong in the final result?

class Coffee: def make(self) -> str: return "espresso"

class Cappuccino(Coffee): def make(self) -> str: return f"{super().make()} + steamed milk" # → espresso + steamed milk # EXTEND — keep parent work

class Mocha(Coffee): def make(self) -> str: return "chocolate + espresso + milk" # no super() # OVERRIDE — replace

Two directions of extending

POST-process base = super().make() return base + "…" (Cappuccino)

PRE-process if shots < 1: raise … base = super().make() return f"{base} x{shots}" (DoubleShot)

Two classic bugs (act these out)

Bug 1 — silent override super().make() # discarded! return "quiet coffee" Parent ran · result lost

Bug 2 — accidental extend Meant to replace, but called super() from habit → layered / confusing output

Rule of thumb YES → parent's work belongs → EXTEND with super() NO → parent's work is wrong → OVERRIDE · skip super()

__init__ is usually extend class Gauge(Meter): def __init__(self, label, max_v): super().__init__(label) self.max_value = max_v Forgetting super() = half-init

Live demo (8 min) 1. Type Coffee + both kids 2. print each .make() 3. Break SilentCoffee 4. Fix: base = super().make() 5. Gauge __init__

3 · Polymorphism (0:40–1:05)

Polymorphism — one call site, many forms

"Poly-morphism" = many forms. The call looks the same; the object decides.

Key insight sound_off never asks "what kind of alarm?" No if / isinstance ladder. Just d.trigger().

Duck typing Python attribute dispatch cares about behaviour: if it has .trigger(), it runs. Declared type is optional.

Contrast later Nominal checks (isinstance) trust the *name* in the tree. Different tool · different job.

Smell to avoid if isinstance(a, X): … elif isinstance(a, Y): … elif isinstance(a, Z): … → prefer a method / singledispatch

Live ask Add BlinkAlarm with trigger → "flash". Does sound_off change? (No — that's the point.)

Optional: ABC peek from abc import ABC, abstractmethod class Alarm(ABC): @abstractmethod def trigger(self): ... Can't instantiate until subclass implements it. Recognition only today.

Checkpoint Can students explain: "same call, different object, different behaviour" in one sentence?

class Alarm: def trigger(self): raise NotImplementedError class LoudAlarm(Alarm): def trigger(self): return "BUZZ" class QuietAlarm(Alarm): def trigger(self): return "blink" def sound_off(devices): return [d.trigger() for d in devices] print(sound_off([LoudAlarm(), QuietAlarm()])) # ['BUZZ', 'blink']

1 · Warm-up (0:00–0:10)

Warm-up — inheritance in one picture

Ask: what do you already get "for free" from a parent class?

Coffee make() → espresso

Cappuccino extend + milk

Mocha override recipe

Live ask "If Cappuccino is a Coffee, what methods does it have before we write anything?"

Bridge from classes • attributes + methods • __init__ sets state • Today: reuse + specialise

Vocabulary

subclass class Child(Parent):

override same name · replace body · no super()

extend same name · call super() · add steps

polymorphism one call · many forms

MRO ordered list Python searches for a method

nominal checks trust declared hierarchy by name

5 · MRO (1:25–1:45)

MRO — method resolution order

super() means "next class in the MRO" — not "my literal parent".

Diamond — predict before you run

A

B

C

D

class D(B, C) MRO → (D, B, C, A, object) ping → D B C A After B, super() hops to C — not to A!

Swap parents class D(C, B) MRO → (D, C, B, A, object) output → D C B A Listed order is a contract.

How to inspect Klass.__mro__ Klass.mro() help(Klass)

Cooperative MI rule Every class in a diamond should call super() for chained methods / __init__. Skip once → silent holes.

Teaching move 1. Cover output 2. Students write prediction 3. Run · compare to __mro__ 4. Swap (C,B) · re-predict

Keep diamonds shallow Prefer mixins: small helpers, often no __init__. Deep webs = MRO surprises.

# Single inheritance — boring & clear class A: def ping(self): print("A") class B(A): def ping(self): print("B"); super().ping() class C(B): def ping(self): print("C"); super().ping() C().ping() # C B A print(C.__mro__) # (C, B, A, object)

4 · Nominal checks (1:05–1:25)

Nominal polymorphism — isinstance / type / issubclass

Nominal = name-based. These checks trust the declared class hierarchy.

Pick the right tool

isinstance(obj, Cls) "this class OR anything declared under it" Lenient · usual choice

type(obj) is Cls "exactly this class" Rejects subclasses on purpose

issubclass(A, B) About classes, not instances Hierarchy question

Trap: Object vs object object (lowercase) is the root of every class. Object → NameError isinstance("a", object) → True

Substitutability (LSP lite) Because Berry is declared under Fruit, hand a Berry anywhere a Fruit is expected. isinstance is meaningful only if subclasses keep promises.

Live drill (10 min) Predict each line before run. Then flip: invent ExactBerry check that rejects subclasses. When would you want that?

Good uses of isinstance • dispatch on declared family • one special-case fallback • accept Base + children Bad: growing if/elif ladder

Board vote b = Berry() isinstance(b, Fruit)? type(b) is Fruit? Hands up before reveal.

class Fruit: pass class Berry(Fruit): pass b = Berry() isinstance(b, Berry) # True isinstance(b, Fruit) # True ← subclasses count type(b) is Berry # True type(b) is Fruit # False ← exact class only issubclass(Berry, Fruit) # True

6 · Diamond lab (1:45–1:50)

Lab — cooperative __init__ through a diamond

Every class sets one field and calls super().__init__(). Watch the MRO order.

Break it on purpose Comment out super() in B. Re-run. Which fields vanish? Why?

Expected after break C never runs A never runs → no c, no a Silent · no exception That's the danger.

Also try Point3D.dump() extend pattern from m75 examples: data = super().dump() data.update({...}) return data

Time box: 5 min pair work then 2 min share

class A: def __init__(self): print("A init"); self.a = "a" class B(A): def __init__(self): print("B init"); super().__init__(); self.b = "b" class C(A): def __init__(self): print("C init"); super().__init__(); self.c = "c" class D(B, C): def __init__(self): print("D init"); super().__init__(); self.d = "d" d = D() print(d.__dict__) # D init → B init → C init → A init # {'a':'a','c':'c','b':'b','d':'d'}

7 · Practice & wrap (1:50–2:00)

Practice check + takeaways

Use Matheion quiz m75 (8 items). Attempt before Discuss / reveal.

Q1 subclass __init__ → usually super().__init__(...)

Q2 isinstance(Berry(), Fruit) → True

Q3 override = replace; extend = super() + add

Q4 device.trigger() many ways → polymorphism

Q5 type(Berry()) is Fruit → False (exact)

Q6 D(B,C) MRO → (D, B, C, A, object)

Q7 isinstance is nominal (declared names)

Q8 super().save() then log → extending

Exit tickets (say out loud)

1. One sentence: override vs extend

2. isinstance vs type(...) is — when?

3. What does super() mean in a diamond?

4. Why keep calling super() in __init__?

Homework / self-study • Type diamond · predict · run • Swap D(C,B) · re-predict • __init__ diamond · all fields • Convert one isinstance ladder → polymorphic method Material: m75 concept + examples

Quick reference

Quick reference

Child keeps parent result → EXTEND · call super() · use return value

Child prepares inputs first → EXTEND · validate · then super()

Parent behaviour is wrong → OVERRIDE · leave super() out

This type or under it → isinstance(obj, Cls)

Exactly this class → type(obj) is Cls

Is class under another? → issubclass(A, B)

See dispatch order → Klass.__mro__

Growing isinstance ladder → → method / singledispatch