Session 26 - Inheritance & Composition

Session 26 of the Python Fundamentals series, covered in depth. It covers two ways to reuse classes: inheritance, where a Dog IS-A Animal, through subclassing, overriding methods, and super() to reuse the parent; and composition, where a Car HAS-A Engine, building objects out of other objects. It also covers isinstance and the is-a test, why you should prefer composition to inheritance, and how method resolution walks the class chain. The traps are forgetting super().__init__, so that the parent's attributes are never set and you hit an AttributeError, deep inheritance chains, and overriding a method without calling super(), which silently loses the parent's behavior. Every snippet and error message was executed and copied verbatim from CPython 3.12.

Subject: Python Fundamentals · 103 slides · code lesson

Open the interactive version of this deck · Homework for this lesson

What this lesson covers

The lesson, slide by slide

1. Inheritance & Composition

Title

Python Fundamentals - Session 26

Two ways to build a class out of another class

2. What you will be able to do

Objectives

You can already write a class with __init__ and methods. Now you will reuse one class to build another. By the end you can:

  1. Make a subclass with class Dog(Animal): and inherit the parent's methods.
  2. Override a method, and call super() to keep the parent's behavior.
  3. Use super().__init__() so parent attributes actually get set.
  1. Test the is-a relationship with isinstance.
  2. Build a class by composition (has-a) - a Car that holds an Engine.
  3. Choose composition over inheritance, and spot the missing-super AttributeError.

3. What survived from Session 25 - Dunder Methods?

Warm-up

Discussion prompt

Before we open Session 26 - Inheritance & Composition: without looking back, what was the main idea of Session 25 - Dunder Methods, and what could you do by the end of it that you could not do before?

Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.

Answer:

Session 25 of the Python Fundamentals series, in depth. Special double-underscore methods that hook Python's own syntax onto your classes: __init__ (already known), __str__ vs __repr__ (print and the shell vs containers), __eq__ for value equality, __len__ for len(), __lt__ for sorting, and __add__ for +.

4. Reusing a Class

Section

Part 1

5. Two ways to reuse a class

Concept

You rarely build every class from scratch. Two tools let one class build on another: inheritance and composition.

Inheritance (is-a)
A Dog IS-A Animal. The subclass gets the parent's methods.
Composition (has-a)
A Car HAS-A Engine. One object holds another as an attribute.

6. Break it if you can: Two ways to reuse a class

Counterexample

Discussion prompt

You rarely build every class from scratch. Two tools let one class build on another: inheritance and composition.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

7. Inheritance is the is-a relationship

Concept

Use inheritance when the new class truly is a kind of the old one. A dog is an animal; a manager is an employee.

inheritance — Defining a class from a parent class so it automatically gets the parent's attributes and methods. The new class is a subclass (or child); the original is the superclass (or parent).

8. By analogy: Inheritance is the is-a relationship

Analogy

Discussion prompt

Explain Inheritance is the is-a relationship by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.

Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.

Answer:

Use inheritance when the new class truly is a kind of the old one. A dog is an animal; a manager is an employee.

9. A subclass is a specialized version

Intuition

Picture a general blueprint (Animal) and a more specific one that starts from it (Dog). The Dog blueprint says 'everything an Animal has, plus these extras.'

You do not recopy the shared parts. You inherit them, then add or change only what makes a Dog special.

10. Teach it back: A subclass is a specialized version

Explain it

Discussion prompt

Explain A subclass is a specialized version to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.

Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.

Answer:

Picture a general blueprint (Animal) and a more specific one that starts from it (Dog). The Dog blueprint says 'everything an Animal has, plus these extras.'

11. Making a Subclass

Section

Part 2

12. class Dog(Animal) creates a subclass

Concept

Put the parent's name in parentheses after the class name: class Dog(Animal):. Now Dog starts with everything Animal has.

Without extra code, a subclass already works - it borrows the parent's __init__ and methods.

13. What has to happen first: A subclass inherits everything

Ranking

Put in order

Put the moves of A subclass inherits everything into the order they have to happen.

  1. Fish adds nothing but (Animal)
  2. Creating a Fish runs Animal's __init__
  3. Read the output

Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. The empty body still inherits __init__ and speak from Animal.

14. A subclass inherits everything

Worked example

class Animal:
    def __init__(self, name):
        self.name = name
    def speak(self):
        return self.name + " makes a sound"

class Fish(Animal):
    pass

f = Fish("Nemo")
print(f.name)
print(f.speak())

Fish adds nothing but (Animal)

Why: The empty body still inherits __init__ and speak from Animal.

Creating a Fish runs Animal's __init__

Why: Fish("Nemo") uses the parent's __init__, so f.name is set.

Read the output

Why: Verified by execution: Nemo, then Nemo makes a sound - both borrowed from Animal.

callcomes fromresult
Fish("Nemo")Animal.__init__name = Nemo
f.nameinheritedNemo
f.speak()Animal.speakNemo makes a sound

15. Fill in: comes from for A subclass inherits everything

Comparison

Comparison matrix

From A subclass inherits everything: refill the comes from column from what you know. The rest of the table is as it appeared.

callcomes fromresult
Fish("Nemo")Animal.__init__name = Nemo
f.nameinheritedNemo
f.speak()Animal.speakNemo makes a sound

16. The subclass can add its own methods

Concept

Inheritance is a starting point, not a cage. A subclass keeps the parent's methods and can define brand-new ones of its own.

17. Plan first: A subclass with an extra method

Step zero

Discussion prompt

A subclass with an extra method — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.

Hint: It starts with: Dog inherits speak and adds fetch

Answer:

  1. Dog inherits speak and adds fetch
  2. Both methods can use self.name
  3. Read the output

18. A subclass with an extra method

Worked example

class Animal:
    def __init__(self, name):
        self.name = name
    def speak(self):
        return self.name + " makes a sound"

class Dog(Animal):
    def fetch(self):
        return self.name + " fetches the ball"

d = Dog("Rex")
print(d.speak())
print(d.fetch())

Dog inherits speak and adds fetch

Why: speak comes from Animal; fetch is new and only Dogs have it.

Both methods can use self.name

Why: self.name was set by Animal's inherited __init__.

Read the output

Why: Verified by execution: Rex makes a sound, then Rex fetches the ball.

calldefined inresult
d.speak()AnimalRex makes a sound
d.fetch()DogRex fetches the ball

19. What each one costs: A subclass with an extra method

Trade off

Comparison matrix

From A subclass with an extra method: every row here is a choice with a cost. Fill the defined in column, then say which row you would actually pick and what you give up for it.

calldefined inresult
d.speak()AnimalRex makes a sound
d.fetch()DogRex fetches the ball

20. Overriding Methods

Section

Part 3

21. Redefine a method to override it

Concept

If a subclass defines a method with the same name as the parent's, the subclass version wins for its objects. This is overriding.

overriding — Defining a method in a subclass that replaces the parent's method of the same name. Objects of the subclass run the new version; the parent's own objects are unaffected.

22. Take the definitions apart: inheritance vs overriding

Definition probe

Sort into buckets

Every line below is part of the definition of inheritance or of overriding — one or the other, never both. Put each where it belongs.

inheritance
Defining a class from a parent class so it automatically gets the parent's attributes and methods.; The new class is a subclass (or child); the original is the superclass (or parent).
overriding
Defining a method in a subclass that replaces the parent's method of the same name.; Objects of the subclass run the new version; the parent's own objects are unaffected.
b1
Defining a class from a parent class so it automatically gets the parent's attributes and methods. The new class is a subclass (or child); the original is the superclass (or parent).
b2
Defining a method in a subclass that replaces the parent's method of the same name. Objects of the subclass run the new version; the parent's own objects are unaffected.

23. Restore the missing line: Override speak in Dog

Fill the middle

Fill in the blanks

From Override speak in Dog — one line has had its right-hand side removed. Put it back.

class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return self.name + " makes a sound"

class Dog(Animal):
def speak(self):
return self.name + " says woof"

a = Animal("Generic")
d = Dog("Rex")
print(a.speak())
print(d.speak())

Why: a is what everything below it consumes, so the wrong expression here fails later and somewhere else. Same name as Animal.speak, so it overrides it for Dog objects.

24. Override speak in Dog

Worked example

class Animal:
    def __init__(self, name):
        self.name = name
    def speak(self):
        return self.name + " makes a sound"

class Dog(Animal):
    def speak(self):
        return self.name + " says woof"

a = Animal("Generic")
d = Dog("Rex")
print(a.speak())
print(d.speak())

Dog defines its own speak

Why: Same name as Animal.speak, so it overrides it for Dog objects.

Each object runs its class's version

Why: a is an Animal, so a.speak() uses Animal's; d is a Dog, so d.speak() uses Dog's.

Read the output

Why: Verified by execution: Generic makes a sound, then Rex says woof.

objectclassspeak() result
aAnimalGeneric makes a sound
dDogRex says woof

25. Inspect it line by line: Override speak in Dog

Error analysis

Annotate

Walk the callouts on Override speak in Dog. Each one is a place this is easy to get subtly wrong.

  • Same name as Animal.speak, so it overrides it for Dog objects.
  • a is an Animal, so a.speak() uses Animal's; d is a Dog, so d.speak() uses Dog's.
  • Verified by execution: Generic makes a sound, then Rex says woof.

26. Override replaces, unless you call super

Concept

By default an override fully replaces the parent method - the parent's code does not run at all.

To reuse the parent's work and add to it, call super().method() inside your override.

27. Where does each piece belong: Session 26 - Inheritance & Composition

Sorting

Sort into buckets

These are the pieces of Session 26 - Inheritance & Composition, out of order. Put each one back under the part of the lesson it belongs to.

Reusing a Class
Two ways to reuse a class; Inheritance is the is-a relationship; A subclass is a specialized version
Making a Subclass
class Dog(Animal) creates a subclass; A subclass inherits everything; The subclass can add its own methods
Overriding Methods
Redefine a method to override it; Override speak in Dog; Override replaces, unless you call super
s1
Reusing a Class is where Session 26 - Inheritance & Composition puts Two ways to reuse a class, Inheritance is the is-a relationship, A subclass is a specialized version. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s2
Making a Subclass is where Session 26 - Inheritance & Composition puts class Dog(Animal) creates a subclass, A subclass inherits everything, The subclass can add its own methods. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s3
Overriding Methods is where Session 26 - Inheritance & Composition puts Redefine a method to override it, Override speak in Dog, Override replaces, unless you call super. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.

28. Finish it with less help: Extend the parent with super()

Faded example

Fill in the blanks

Extend the parent with super(), with the scaffolding fading: two lines are gone now — fill both.

class Greeter:
def __init__(self, name):
self.name = name
def greet(self):
return "Hi, " + self.name

class LoudGreeter(Greeter):
def greet(self):
base = super().greet()
return base + "!!!"

print(LoudGreeter("Ana").greet())

Why: Reproducing these unaided, rather than reading them, is what tells you the method has transferred. It returns "Hi, Ana" without recopying that logic.

29. Extend the parent with super()

Worked example

class Greeter:
    def __init__(self, name):
        self.name = name
    def greet(self):
        return "Hi, " + self.name

class LoudGreeter(Greeter):
    def greet(self):
        base = super().greet()
        return base + "!!!"

print(LoudGreeter("Ana").greet())

super().greet() runs the parent's greet

Why: It returns "Hi, Ana" without recopying that logic.

The override adds to the parent's result

Why: It appends "!!!" to whatever the parent produced.

Read the output

Why: Verified by execution: Hi, Ana!!! - parent behavior plus the extra.

stepvalue
super().greet()Hi, Ana
base + "!!!"Hi, Ana!!!

30. Draw the shape of it: Extend the parent with super()

Blank canvas

Draw it

Draw what Extend the parent with super() just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.

31. super().__init__

Section

Part 4

32. super().__init__ reuses the parent's setup

Concept

When a subclass writes its own __init__, it replaces the parent's. Call super().__init__(...) first to let the parent set its attributes.

super().__init__() — A call inside a subclass's __init__ that runs the parent class's __init__, so the parent's attributes (like self.name) get set before the subclass adds its own.

33. Dog adds a breed on top of Animal

Worked example

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

d = Dog("Rex", "Corgi")
print(d.name)
print(d.breed)

super().__init__(name) sets self.name

Why: It runs Animal's __init__, which stores name.

Then Dog sets its own attribute

Why: self.breed is the part that is special to Dog.

Read the output

Why: Verified by execution: Rex, then Corgi - both attributes exist.

attributeset byvalue
d.nameAnimal.__init__Rex
d.breedDog.__init__Corgi

34. super() works in any method, not just __init__

Concept

You can call super().anything() to reach the parent's version of that method. It is the general way to say 'do what the parent does, then add mine.'

35. Plan first: A Manager whose pay adds a bonus

Step zero

Discussion prompt

A Manager whose pay adds a bonus — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.

Hint: It starts with: __init__ reuses the parent, then adds bonus

Answer:

  1. __init__ reuses the parent, then adds bonus
  2. pay builds on the parent's pay
  3. Read the output

36. A Manager whose pay adds a bonus

Worked example

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    def pay(self):
        return self.salary

class Manager(Employee):
    def __init__(self, name, salary, bonus):
        super().__init__(name, salary)
        self.bonus = bonus
    def pay(self):
        return super().pay() + self.bonus

m = Manager("Sam", 5000, 1000)
print(m.name)
print(m.pay())

__init__ reuses the parent, then adds bonus

Why: super().__init__ sets name and salary; Manager adds bonus.

pay builds on the parent's pay

Why: super().pay() returns the salary; Manager adds the bonus on top.

Read the output

Why: Verified by execution: Sam, then 6000 (5000 + 1000).

stepvalue
super().pay()5000
+ self.bonus6000
m.nameSam

37. Watch it run: A Manager whose pay adds a bonus

Pattern

Step through it

Step through A Manager whose pay adds a bonus one row at a time. What is driving the change, and what would the row after the last one be?

  1. Step 1: step is super().pay()
  2. Step 2: step is + self.bonus
  3. Step 3: step is m.name

38. Combine super().__init__ with an override

Concept

A subclass often does both at once: call super().__init__(...) to set the shared attributes, and override a method to change one behavior.

The parent supplies the common frame; the subclass fills in the one part that is different.

39. Restore the missing line: A Rectangle that overrides area

Fill the middle

Fill in the blanks

From A Rectangle that overrides area — one line has had its right-hand side removed. Put it back.

class Shape:
def __init__(self, color):
self.color = color
def area(self):
return 0

class Rectangle(Shape):
def __init__(self, color, w, h):
super().__init__(color)
self.w = w
self.h = h
def area(self):
return self.w * self.h

r = Rectangle("red", 3, 4)
print(r.color)
print(r.area())

Why: self.color is what everything below it consumes, so the wrong expression here fails later and somewhere else. Shape stores color; Rectangle then adds w and h.

40. A Rectangle that overrides area

Worked example

class Shape:
    def __init__(self, color):
        self.color = color
    def area(self):
        return 0

class Rectangle(Shape):
    def __init__(self, color, w, h):
        super().__init__(color)
        self.w = w
        self.h = h
    def area(self):
        return self.w * self.h

r = Rectangle("red", 3, 4)
print(r.color)
print(r.area())

super().__init__ sets the shared color

Why: Shape stores color; Rectangle then adds w and h.

area is overridden with real math

Why: Shape.area returns 0; Rectangle replaces it with w * h.

Read the output

Why: Verified by execution: red, then 12 (3 * 4).

callcomes fromresult
r.colorShape.__init__red
r.area()Rectangle.area12

41. The Missing super Trap

Section

Part 5

42. Something is wrong here: forgetting super().__init__

Anomaly

Predict first

A student writes this, and it looks reasonable:

The subclass writes its own __init__ but never calls the parent's.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: Dog's __init__ replaced Animal's and skipped super().__init__(name), so name was never stored.

Call super().__init__ so the parent sets its attributes.

Why: Dog's __init__ replaced Animal's and skipped super().__init__(name), so name was never stored. breed prints fine, then name crashes.

43. Trap: forgetting super().__init__

Trap

The trap

The subclass writes its own __init__ but never calls the parent's.

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        self.breed = breed

d = Dog("Rex", "Corgi")
print(d.breed)
print(d.name)

self.name is never set

Why: Dog's __init__ replaced Animal's and skipped super().__init__(name), so name was never stored. breed prints fine, then name crashes.

lineresult
print(d.breed)Corgi
print(d.name)AttributeError: 'Dog' object has no attribute 'name'

The fix

Call super().__init__ so the parent sets its attributes.

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

d = Dog("Rex", "Corgi")
print(d.breed)
print(d.name)

Now both attributes exist

Why: super().__init__(name) stores name; Dog stores breed. Real output: Corgi, then Rex. Rule of thumb: if a subclass defines __init__, its first line is usually super().__init__(...).

lineresult
print(d.breed)Corgi
print(d.name)Rex

44. Break it on purpose: forgetting super().__init__

Break the constraint

Discussion prompt

The rule this trap just fixed:

Call super().__init__ so the parent sets its attributes.

Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?

Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.

Answer:

Dog's __init__ replaced Animal's and skipped super().__init__(name), so name was never stored. breed prints fine, then name crashes.

45. The error appears later than the mistake

Concept

The missing super().__init__ does not crash right away. The object is built fine - the crash comes the moment something reads the attribute that was never set.

So AttributeError: '...' object has no attribute 'name' often points back to a forgotten super() call.

46. Same trap, hidden inside a method

Worked example

class Logger:
    def __init__(self):
        self.entries = []
    def log(self, msg):
        self.entries.append(msg)

class TimeLogger(Logger):
    def __init__(self):
        pass

t = TimeLogger()
t.log("hello")

TimeLogger.__init__ does nothing useful

Why: pass means no super().__init__(), so self.entries is never created.

The crash waits until log() runs

Why: Creating t is fine; the AttributeError only fires when log tries to use self.entries.

Read the traceback

Why: Verified by execution: AttributeError: 'TimeLogger' object has no attribute 'entries'.

stepresult
TimeLogger()object built (no entries)
t.log("hello")AttributeError: 'TimeLogger' object has no attribute 'entries'

47. Init runs from the outside in

Concept

Usually the subclass's __init__ runs first, calls super().__init__() to run the parent's, then finishes its own setup.

You control the order: put super().__init__() where the parent's attributes need to exist before your code uses them.

48. What has to happen first: Trace the __init__ call order

Ranking

Put in order

Put the moves of Trace the __init__ call order into the order they have to happen.

  1. Child.__init__ starts first
  2. super().__init__() hands control to Base
  3. Read the output

Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. Child() calls Child's __init__, which prints Child init.

49. Trace the __init__ call order

Worked example

class Base:
    def __init__(self):
        print("Base init")
        self.ok = True

class Child(Base):
    def __init__(self):
        print("Child init")
        super().__init__()

Child()

Child.__init__ starts first

Why: Child() calls Child's __init__, which prints Child init.

super().__init__() hands control to Base

Why: Then Base's __init__ runs, prints Base init, and sets self.ok.

Read the output

Why: Verified by execution: Child init, then Base init - in that order.

orderrunsprints
1Child.__init__Child init
2Base.__init__Base init

50. isinstance & is-a

Section

Part 6

51. isinstance tests the is-a relationship

Concept

isinstance(obj, Class) is True when obj is that class or any subclass of it. It answers 'is this object a kind of Class?'

isinstance(obj, C) — A built-in that returns True if obj is an instance of class C or of any subclass of C. It reflects the is-a relationship set up by inheritance.

52. State the rule before it runs: isinstance across the family

Hypothesis

Predict first

isinstance across the family is about to be worked. State your hypothesis first: which rule or definition decides this one, and what is the first move it forces? Then watch whether the example agrees with you.

Correct: A Dog is a Dog and an Animal

Why: Both are True - Dog inherits from Animal, so the is-a chain holds upward.

A hypothesis you wrote down is falsifiable; a vague sense of how it will go is not. If the example opens somewhere else, that gap is the thing worth chasing.

53. isinstance across the family

Worked example

class Animal:
    pass
class Dog(Animal):
    pass

d = Dog()
a = Animal()
print(isinstance(d, Dog))
print(isinstance(d, Animal))
print(isinstance(a, Dog))
print(isinstance(d, object))

A Dog is a Dog and an Animal

Why: Both are True - Dog inherits from Animal, so the is-a chain holds upward.

But an Animal is not a Dog

Why: The relationship is one-way: every Dog is an Animal, not every Animal is a Dog.

Read the output

Why: Verified by execution: True, True, False, True (everything is an object).

checkresult
isinstance(d, Dog)True
isinstance(d, Animal)True
isinstance(a, Dog)False
isinstance(d, object)True

54. Which is which, by result

Discrimination

Sort into buckets

Sort these by result, from memory, without looking back at isinstance across the family. Telling them apart on the spot is the skill; the table is only where the answer happens to be written down.

True
isinstance(d, Dog); isinstance(d, Animal); isinstance(d, object)
False
isinstance(a, Dog)
g1
result is "True" for isinstance(d, Dog), isinstance(d, Animal), isinstance(d, object) — that is what the table on "isinstance across the family" records, and it is the single property separating this group from the rest.
g2
result is "False" for isinstance(a, Dog) — that is what the table on "isinstance across the family" records, and it is the single property separating this group from the rest.

55. isinstance beats == type

Concept

type(d) == Animal is False even when d is a Dog, because the exact type is Dog. isinstance respects subclasses; type == does not.

Pass a tuple to check several classes at once: isinstance(d, (Dog, Cat)).

56. Predict the next row: isinstance vs type ==

Pattern

Predict first

The table runs: isinstance(d, (Dog, Cat)) | True · type(d) == Animal | False

In isinstance vs type ==, given the rows so far: what is the next one — the row where check is type(d) == Dog?

Correct: type(d) == Dog | True

checkresult
isinstance(d, (Dog, Cat))True
type(d) == AnimalFalse
type(d) == DogTrue

Why: The relationship between the columns, not the individual numbers, is what generates the next row. d is a Dog, and Dog is in the tuple, so it is True.

57. isinstance vs type ==

Worked example

class Animal: pass
class Dog(Animal): pass
class Cat(Animal): pass

d = Dog()
print(isinstance(d, (Dog, Cat)))
print(type(d) == Animal)
print(type(d) == Dog)

A tuple asks 'is it any of these?'

Why: d is a Dog, and Dog is in the tuple, so it is True.

type == checks the exact class only

Why: d's exact type is Dog, not Animal, so type(d) == Animal is False.

Read the output

Why: Verified by execution: True, False, True.

checkresult
isinstance(d, (Dog, Cat))True
type(d) == AnimalFalse
type(d) == DogTrue

58. Fill in: result for isinstance vs type ==

Comparison

Comparison matrix

From isinstance vs type ==: refill the result column from what you know. The rest of the table is as it appeared.

checkresult
isinstance(d, (Dog, Cat))True
type(d) == AnimalFalse
type(d) == DogTrue

59. Composition: has-a

Section

Part 7

60. Composition: an object holds other objects

Concept

Instead of inheriting, a class can contain another object as an attribute. A Car HAS-A Engine; the Car stores an Engine and uses it.

composition — Building a class by giving it other objects as attributes (has-a), rather than by subclassing (is-a). The container delegates work to the objects it holds.

61. Term to definition: Session 26 - Inheritance & Composition

Matching

Match the pairs

Match each term to the definition this lesson gave it — not the one you would guess from the word.

  • t1. overriding
  • t2. super().__init__()
  • t3. isinstance(obj, C)
  • t4. composition
  • d1. Defining a method in a subclass that replaces the parent's method of the same name. Objects of the subclass run the new version; the parent's own objects are unaffected.
  • d2. A call inside a subclass's __init__ that runs the parent class's __init__, so the parent's attributes (like self.name) get set before the subclass adds its own.
  • d3. A built-in that returns True if obj is an instance of class C or of any subclass of C. It reflects the is-a relationship set up by inheritance.
  • d4. Building a class by giving it other objects as attributes (has-a), rather than by subclassing (is-a). The container delegates work to the objects it holds.

Why: These are the working definitions of overriding, super().__init__(), isinstance(obj, C), composition as Session 26 - Inheritance & Composition uses them. Pairing them correctly is the test of whether you could state each one with the slide switched off.

62. Parts you assemble, not a family you belong to

Intuition

Inheritance says what an object is. Composition says what an object has. A car is not a kind of engine - it has one, plus wheels, plus seats.

When you catch yourself saying 'has a', reach for composition; save inheritance for real 'is a' cases.

63. Plan first: A Car HAS-A Engine

Step zero

Discussion prompt

A Car HAS-A Engine — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.

Hint: It starts with: Car builds an Engine and stores it

Answer:

  1. Car builds an Engine and stores it
  2. Car delegates work to its engine
  3. Read the output

64. A Car HAS-A Engine

Worked example

class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower
    def start(self):
        return "engine with " + str(self.horsepower) + " hp starts"

class Car:
    def __init__(self, model, horsepower):
        self.model = model
        self.engine = Engine(horsepower)
    def start(self):
        return self.model + ": " + self.engine.start()

c = Car("Sedan", 120)
print(c.start())
print(c.engine.horsepower)

Car builds an Engine and stores it

Why: self.engine = Engine(horsepower) - the Car HAS an Engine object.

Car delegates work to its engine

Why: Car.start() asks self.engine to start, then wraps the result.

Read the output

Why: Verified by execution: Sedan: engine with 120 hp starts, then 120.

calldelegates toresult
c.start()self.engine.start()Sedan: engine with 120 hp starts
c.engine.horsepowerthe held Engine120

65. Finish it with less help: An Order HAS-A Customer

Faded example

Fill in the blanks

An Order HAS-A Customer, with the scaffolding fading: two lines are gone now — fill both.

class Customer:
def __init__(self, name):
self.name = name

class Order:
def __init__(self, customer, total):
self.customer = customer
self.total = total
def summary(self):
return self.customer.name + " owes " + str(self.total)

c = Customer("Dana")
o = Order(c, 40)
print(o.summary())

Why: Reproducing these unaided, rather than reading them, is what tells you the method has transferred. Order stores an existing Customer object as self.customer.

66. An Order HAS-A Customer

Worked example

class Customer:
    def __init__(self, name):
        self.name = name

class Order:
    def __init__(self, customer, total):
        self.customer = customer
        self.total = total
    def summary(self):
        return self.customer.name + " owes " + str(self.total)

c = Customer("Dana")
o = Order(c, 40)
print(o.summary())

The Customer is passed in, not inherited

Why: Order stores an existing Customer object as self.customer.

Order reaches into its Customer

Why: summary reads self.customer.name - the held object's attribute.

Read the output

Why: Verified by execution: Dana owes 40.

expressionvalue
o.customerthe Customer object
o.customer.nameDana
o.summary()Dana owes 40

67. A container can hold many objects

Concept

Composition scales up: a class can hold a list of other objects and loop over them - a Bicycle with two Wheels, a Playlist of Songs.

68. Predict the next row: A Bicycle HAS two Wheels

Pattern

Predict first

The table runs: b.wheel_count() | 2 · b.wheels[0] | a Wheel object

In A Bicycle HAS two Wheels, given the rows so far: what is the next one — the row where expression is b.wheels[0].size?

Correct: b.wheels[0].size | 26

expressionvalue
b.wheel_count()2
b.wheels[0]a Wheel object
b.wheels[0].size26

Why: The relationship between the columns, not the individual numbers, is what generates the next row. self.wheels is a list; each item is its own Wheel.

69. A Bicycle HAS two Wheels

Worked example

class Wheel:
    def __init__(self, size):
        self.size = size

class Bicycle:
    def __init__(self):
        self.wheels = [Wheel(26), Wheel(26)]
    def wheel_count(self):
        return len(self.wheels)

b = Bicycle()
print(b.wheel_count())
print(b.wheels[0].size)

Bicycle holds a list of Wheel objects

Why: self.wheels is a list; each item is its own Wheel.

Reach a held object through the list

Why: b.wheels[0] is the first Wheel; .size reads its attribute.

Read the output

Why: Verified by execution: 2, then 26.

expressionvalue
b.wheel_count()2
b.wheels[0]a Wheel object
b.wheels[0].size26

70. Prefer Composition

Section

Part 8

71. Prefer composition over inheritance

Concept

A common guideline: when both could work, reach for composition. Holding an object is more flexible than being locked into a parent's whole interface.

Use inheritance only for a genuine is-a. If you would say 'has a' or 'uses a', compose instead.

72. Teach it back: Prefer composition over inheritance

Explain it

Discussion prompt

Explain Prefer composition over inheritance to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.

Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.

Answer:

A common guideline: when both could work, reach for composition. Holding an object is more flexible than being locked into a parent's whole interface.

73. Restore the missing line: Wrong is-a: a Car is not an Engine

Fill the middle

Fill in the blanks

From Wrong is-a: a Car is not an Engine — one line has had its right-hand side removed. Put it back.

class Engine:
def start(self):
return "vroom"

class Car(Engine):
pass

c = Car()
print(c.start())
print(isinstance(c, Engine))

Why: c is what everything below it consumes, so the wrong expression here fails later and somewhere else. Car(Engine) makes Python treat a Car AS an Engine - isinstance says True, which is nonsense in the real world.

74. Wrong is-a: a Car is not an Engine

Worked example

class Engine:
    def start(self):
        return "vroom"

class Car(Engine):
    pass

c = Car()
print(c.start())
print(isinstance(c, Engine))

It runs, but the model is wrong

Why: Car(Engine) makes Python treat a Car AS an Engine - isinstance says True, which is nonsense in the real world.

Composition would say Car HAS an Engine

Why: self.engine = Engine() keeps them separate, which matches reality.

Read the output

Why: Verified by execution: vroom, then True - it works, but forces a false is-a.

checkresultsensible?
c.start()vroomborrowed
isinstance(c, Engine)Trueno - a car is not an engine

75. Something is wrong here: overriding without super() loses behavior

Anomaly

Predict first

A student writes this, and it looks reasonable:

The override ignores the parent entirely, dropping the name it was built with.

It is wrong. Say what breaks — and say it before you turn the page.

Correct: greet returns a fixed "HELLO" and never uses self.name, so Ana is lost.

Call super() to keep the parent's work, then add yours.

Why: greet returns a fixed "HELLO" and never uses self.name, so Ana is lost. No error - just wrong behavior.

76. Trap: overriding without super() loses behavior

Trap

The trap

The override ignores the parent entirely, dropping the name it was built with.

class Greeter:
    def __init__(self, name):
        self.name = name
    def greet(self):
        return "Hi, " + self.name

class BrokenGreeter(Greeter):
    def greet(self):
        return "HELLO"

print(BrokenGreeter("Ana").greet())

The parent's logic silently vanishes

Why: greet returns a fixed "HELLO" and never uses self.name, so Ana is lost. No error - just wrong behavior.

callresult
BrokenGreeter("Ana").greet()HELLO

The fix

Call super() to keep the parent's work, then add yours.

class Greeter:
    def __init__(self, name):
        self.name = name
    def greet(self):
        return "Hi, " + self.name

class LoudGreeter(Greeter):
    def greet(self):
        return super().greet() + "!!!"

print(LoudGreeter("Ana").greet())

super().greet() keeps the name

Why: It reuses "Hi, Ana", then adds "!!!". Real output: Hi, Ana!!!. Rule of thumb: if your override should extend the parent, start it with super().

callresult
LoudGreeter("Ana").greet()Hi, Ana!!!

77. Which of these survive contact with Session 26 - Inheritance & Composition?

Two truths and a lie

Sort into buckets

Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.

Holds up
You rarely build every class from scratch. Two tools let one class build on another: inheritance and composition.; Use inheritance when the new class truly is a kind of the old one. A dog is an animal; a manager is an employee.; Picture a general blueprint (Animal) and a more specific one that starts from it (Dog). The Dog blueprint says 'everything an Animal has, plus these extras.'
Breaks
The subclass writes its own __init__ but never calls the parent's.; The override ignores the parent entirely, dropping the name it was built with.
sound
These are stated as this lesson states them — each one survives the edge cases Session 26 - Inheritance & Composition puts it through.
flawed
Each of these is lifted from a trap in this deck: reasonable-sounding, and wrong in a way that only shows up once you rely on it.

78. Method Resolution

Section

Part 9

79. Python searches up the chain

Concept

Call obj.method() and Python looks in the object's class first, then its parent, then the parent's parent, up to object. The first match wins.

That search order is the method resolution order (MRO). It is why an override in the subclass beats the parent's version.

80. By analogy: Python searches up the chain

Analogy

Discussion prompt

Explain Python searches up the chain by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.

Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.

Answer:

Call obj.method() and Python looks in the object's class first, then its parent, then the parent's parent, up to object. The first match wins.

81. Plan first: Trace the method lookup

Step zero

Discussion prompt

Trace the method lookup — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.

Hint: It starts with: C has no hello, so Python looks up

Answer:

  1. C has no hello, so Python looks up
  2. __mro__ shows the exact search order
  3. Read the output

82. Trace the method lookup

Worked example

class A:
    def hello(self):
        return "A"

class B(A):
    def hello(self):
        return "B"

class C(B):
    pass

c = C()
print(c.hello())
print(C.__mro__)

C has no hello, so Python looks up

Why: It checks C (nothing), then B (found) - so B's hello wins.

__mro__ shows the exact search order

Why: C, then B, then A, then object - Python walks this list left to right.

Read the output

Why: Verified by execution: B, then (<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>).

class searchedhas hello?used?
Cnokeep looking
Byesused - returns B
Ayesnot reached
objectnonot reached

83. Draw the shape of it: Trace the method lookup

Blank canvas

Draw it

Draw what Trace the method lookup just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.

84. super() follows the same chain

Concept

super() means 'start the search one step above me in the MRO.' That is why super().greet() finds the parent's version, not your own.

85. What has to happen first: super() walks a three-level chain

Ranking

Put in order

Put the moves of super() walks a three-level chain into the order they have to happen.

  1. Each super() climbs one level
  2. Every level sets its own attribute
  3. Read the output

Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. C calls B, B calls A - a chain of super().__init__ calls.

86. super() walks a three-level chain

Worked example

class A:
    def __init__(self):
        self.a = 1

class B(A):
    def __init__(self):
        super().__init__()
        self.b = 2

class C(B):
    def __init__(self):
        super().__init__()
        self.c = 3

c = C()
print(c.a, c.b, c.c)

Each super() climbs one level

Why: C calls B, B calls A - a chain of super().__init__ calls.

Every level sets its own attribute

Why: A sets a, B sets b, C sets c - all present on the final object.

Read the output

Why: Verified by execution: 1 2 3 - all three attributes exist.

attributeset byvalue
c.aA.__init__1
c.bB.__init__2
c.cC.__init__3

87. Inspect it line by line: super() walks a three-level chain

Error analysis

Annotate

Walk the callouts on super() walks a three-level chain. Each one is a place this is easy to get subtly wrong.

  • C calls B, B calls A - a chain of super().__init__ calls.
  • A sets a, B sets b, C sets c - all present on the final object.
  • Verified by execution: 1 2 3 - all three attributes exist.

88. Keep chains shallow

Concept

Deep inheritance (A to B to C to D...) gets hard to follow - a method could live anywhere up the chain, and one missing super() breaks the rest.

Two or three levels is usually plenty. If a hierarchy grows tall, composition is often the cleaner fix.

89. Break it if you can: Keep chains shallow

Counterexample

Discussion prompt

Deep inheritance (A to B to C to D...) gets hard to follow - a method could live anywhere up the chain, and one missing super() breaks the rest.

That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.

Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.

Answer:

Two or three levels is usually plenty. If a hierarchy grows tall, composition is often the cleaner fix.

90. Patterns & Checks

Section

Part 10

91. Writing a subclass

Pattern

1. class Child(Parent): to inherit everything

Why: The subclass starts with the parent's attributes and methods.

2. In __init__, call super().__init__(...) first

Why: That sets the parent's attributes before you add your own; skipping it causes AttributeError later.

3. Override a method by redefining its name

Why: The subclass version wins for its objects.

4. Call super().method() to keep the parent's behavior

Why: Use it whenever your override should extend rather than replace the parent.

92. Inheritance or composition?

Pattern

Say the relationship out loud

Why: 'A Dog IS-A Animal' -> inheritance. 'A Car HAS-A Engine' -> composition.

Genuine is-a? Subclass it

Why: Use inheritance only when the child is truly a kind of the parent.

Otherwise, hold the object as an attribute

Why: Composition (self.thing = Thing()) is more flexible and avoids false is-a relationships.

When in doubt, prefer composition

Why: It keeps classes loosely coupled and hierarchies shallow.

93. Where this shows up: Session 26 - Inheritance & Composition

Real world

Discussion prompt

Outside this lesson: where does Session 26 - Inheritance & Composition actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of Inheritance or composition? is doing the work in it.

Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.

Answer:

Session 26 of the Python Fundamentals series, in depth. Two ways to reuse classes: inheritance (a Dog IS-A Animal - subclassing, overriding methods, and super() to reuse the parent) and composition (a Car HAS-A Engine - building objects out of other objects).

94. Check: which method runs

Check

Cow overrides sound.

class Animal:
    def sound(self):
        return "..."
class Cow(Animal):
    def sound(self):
        return "moo"
print(Cow().sound())
objectclasssound()
Cow()Cow?

Check your understanding

What does this print?

  • A. moo (correct)
  • B. ...
  • C. ...moo
  • D. Error

Answer: A

Why: Cow defines its own sound, which overrides Animal.sound for Cow objects, so it returns moo. Verified by execution.

Why B tempts people
"..." is Animal's version; the override in Cow replaces it for Cow objects.
Why C tempts people
The override does not call super(), so the parent's "..." is not included at all.
Why D tempts people
This is valid overriding, not an error - Cow.sound simply wins.

95. Check: super().__init__

Check

Sub calls super().__init__.

class Base:
    def __init__(self, x):
        self.x = x
class Sub(Base):
    def __init__(self, x, y):
        super().__init__(x)
        self.y = y
s = Sub(1, 2)
print(s.x + s.y)
s.xs.ys.x + s.y
12?

Check your understanding

What does this print?

  • A. 3 (correct)
  • B. AttributeError on s.x
  • C. 12
  • D. 2

Answer: A

Why: super().__init__(x) sets self.x to 1 and Sub sets self.y to 2, so s.x + s.y is 3. Verified by execution.

Why B tempts people
Because super().__init__(x) is called, self.x is set - there is no AttributeError here.
Why C tempts people
The + adds the two numbers (1 + 2 = 3); it does not join them as text.
Why D tempts people
self.x is set to 1 by the super call, so the sum is 3, not just y.

96. Check: the missing super

Check

Stopwatch skips super().__init__.

class Timer:
    def __init__(self):
        self.count = 0
class Stopwatch(Timer):
    def __init__(self):
        self.laps = []
sw = Stopwatch()
print(sw.count)
attributeset?
sw.lapsyes
sw.count?

Check your understanding

What does print(sw.count) do?

  • A. AttributeError: 'Stopwatch' object has no attribute 'count' (correct)
  • B. prints 0
  • C. prints []
  • D. prints None

Answer: A

Why: Stopwatch.__init__ never calls super().__init__(), so self.count is never set and reading it raises AttributeError. Verified by execution.

Why B tempts people
0 would require Timer's __init__ to run, but Stopwatch replaced it and skipped super().
Why C tempts people
[] is sw.laps, not sw.count - count was never created at all.
Why D tempts people
A missing attribute raises AttributeError; it does not default to None.

97. Check: isinstance

Check

Square is a Shape.

class Shape:
    def area(self):
        return 0
class Square(Shape):
    def __init__(self, side):
        self.side = side
    def area(self):
        return self.side * self.side
sq = Square(5)
print(isinstance(sq, Shape))
objectclassisinstance(sq, Shape)
sqSquare?

Check your understanding

What does this print?

  • A. True (correct)
  • B. False
  • C. 25
  • D. Error

Answer: A

Why: Square is a subclass of Shape, so a Square object is-a Shape and isinstance returns True. Verified by execution.

Why B tempts people
isinstance follows subclasses, and Square inherits from Shape, so it is True, not False.
Why C tempts people
25 is what sq.area() returns; isinstance returns a bool, not the area.
Why D tempts people
isinstance with a class is valid and returns a boolean - no error.

98. Check: composition

Check

Camera holds a Phone.

class Phone:
    def __init__(self):
        self.battery = 100
class Camera:
    def __init__(self):
        self.phone = Phone()
cam = Camera()
print(cam.phone.battery)
print(isinstance(cam, Phone))
expressionvalue
cam.phone.battery?
isinstance(cam, Phone)?

Check your understanding

What are the two lines of output?

  • A. 100 then False (correct)
  • B. 100 then True
  • C. AttributeError then False
  • D. 0 then True

Answer: A

Why: Camera HAS-A Phone via composition, so cam.phone.battery is 100; but Camera does not inherit Phone, so isinstance(cam, Phone) is False. Verified by execution.

Why B tempts people
Composition is not inheritance - Camera holds a Phone but is not a Phone, so isinstance is False.
Why C tempts people
cam.phone is a real Phone object with battery set to 100, so there is no AttributeError.
Why D tempts people
battery is set to 100 in Phone's __init__, not 0.

99. What each one costs: Check: composition

Trade off

Comparison matrix

From Check: composition: every row here is a choice with a cost. Fill the value column, then say which row you would actually pick and what you give up for it.

expressionvalue
cam.phone.battery?
isinstance(cam, Phone)?

100. Check: super() in a method

Check

B extends A's greet.

class A:
    def greet(self):
        return "hello from A"
class B(A):
    def greet(self):
        return super().greet() + " and B"
print(B().greet())
stepvalue
super().greet()hello from A
result?

Check your understanding

What does this print?

  • A. hello from A and B (correct)
  • B. and B
  • C. hello from A
  • D. hello from B and B

Answer: A

Why: super().greet() returns "hello from A", and B appends " and B", giving hello from A and B. Verified by execution.

Why B tempts people
super().greet() is not empty - it returns "hello from A", which is kept in the result.
Why C tempts people
B's override adds " and B" to the parent's result, so the string does not stop at A.
Why D tempts people
super().greet() runs A's version ("hello from A"), not B's, so there is no second B.

101. Fill in: value for Check: super() in a method

Comparison

Comparison matrix

From Check: super() in a method: refill the value column from what you know. The rest of the table is as it appeared.

stepvalue
super().greet()hello from A
result?

102. Connect it up: Session 26 - Inheritance & Composition

Connect it up

Draw it

One page, no notation unless you need it: draw how these connect — Reusing a Class · Making a Subclass · Overriding Methods · super().__init__ · The Missing super Trap · isinstance & is-a. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.

103. What you can do now

Recap

You can reuse classes two ways: inheritance (a subclass IS-A parent) and composition (a class HAS-A other object as an attribute).

You writeIt means
class Dog(Animal):Dog inherits Animal's attributes and methods
def speak(self): ...override - Dog's version wins for Dog objects
super().__init__(name)run the parent's __init__ so its attributes get set
super().greet()reuse the parent's method, then add to it
isinstance(d, Animal)True if d is an Animal or a subclass of it
self.engine = Engine()composition - the object HAS an Engine

Prefer composition unless it is a genuine is-a; always call super().__init__ or the missing attribute will crash later. Next session we build a full project using these tools together.

Sources

  1. Python 3 Tutorial - Inheritance
  2. Python 3 - super()
  3. Python 3 - isinstance()
  4. All snippets and error messages executed and copied from CPython 3.12. — Author verification run, 2026-07-15 (Python Fundamentals series, Session 26).

Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108