18c Inheritance, Class Diagrams, and Data Encapsulation

This lesson defines a class as a modified version of another, overrides an inherited method, reads class diagrams showing IS-A and HAS-A relationships, and discovers a class interface by encapsulating global state.

Subject: Python · 65 slides · code lesson

Open the interactive version of this deck

What this lesson covers

The lesson, slide by slide

1. Lesson 18c Inheritance, Class Diagrams, and Data Encapsulation

Title

Python · Chapter 18 — Inheritance

§18.7-18.10, pp. 176-179

2. By the end of this lesson you can

Objectives

Five things, each one you can check yourself at an interpreter prompt.

Think Python, 2nd edition — Allen B. Downey §18.7-18.10, pp. 176-179 — the pages these objectives are drawn from

3. Before we start: a hand of cards

Warm-up

You have a Deck. Now you need a Hand.

Discussion prompt

List three things a hand and a deck have in common, and two things you would want from a hand that make no sense for a deck.

Hint: Both hold cards. Only one is compared with another.

Answer:

Both are made up of a collection of cards, both need adding and removing, and both need printing. Those are the same operations, on the same kind of contents.

But in poker you might compare two hands to see which one wins, and in bridge you might compute a score for a hand in order to make a bid — neither of which makes sense for a deck.

Similar, but different. That relationship between classes is what lends itself to inheritance, which is this lesson.

4. The one idea behind this lesson: a modified version of an existing class

Concept

Inheritance is the ability to define a new class that is a modified version of an existing class. To define a new class that inherits from an existing one, you put the name of the existing class in parentheses.

inheritance — The ability to define a new class that is a modified version of a previously defined class.

class Hand(Deck):
    """Represents a hand of playing cards."""
PartWhat it doesNote
Deck in parenthesesHand inherits from Deckthe relationship
what that meanspop_card and add_card work on Handsinherited
the vocabularyDeck is the parent, Hand the child

When a new class inherits from an existing one, the existing one is called the parent and the new class is called the child. This definition indicates that Hand inherits from Deck, which means we can use methods like pop_card and add_card for Hands as well as Decks.

Figure (svg): A Hand class shown inheriting the Deck class's methods

One class defined as a modified version of another.

Think Python, 2nd edition — Allen B. Downey §18.7-18.10, pp. 176-176

5. Defining a child class

Section

Section 1

6. Similar, but different

Concept

A hand is similar to a deck: both are made up of a collection of cards, and both require operations like adding and removing cards. A hand is also different from a deck; there are operations we want for hands that don't make sense for a deck.

class Hand(Deck):
    """Represents a hand of playing cards."""

>>> hand = Hand()
>>> hand.add_card(card)      # inherited from Deck
>>> print(hand)              # also inherited
PartWhat happensNote
the parenthesesname the parentDeck
the methodsavailable immediatelywithout being written
what is newnothing yetexcept the name

This relationship between classes — similar, but different — lends itself to inheritance. In poker we might compare two hands to see which one wins; in bridge, we might compute a score for a hand in order to make a bid. Neither is meaningful for a deck.

Think Python, 2nd edition — Allen B. Downey §18.7-18.10, pp. 176-176

7. Picture it: what the child gets for free

Picture it

Every method the parent has.

Figure (svg): Two columns showing what a child class inherits and what it must define itself

The child starts with everything the parent has, and changes what it needs to.

Which is the case for inheritance in one picture: the shared operations are written once, and only the differences are new code.

8. Worked example: what a Hand can already do

Worked example

Before a single method is written for it.

>>> deck = Deck()
>>> card = deck.pop_card()
>>> hand.add_card(card)
>>> print(hand)
King of Spades
CallWhere the method comes fromNote
hand.add_cardDeck's methodinherited
print(hand)Deck's __str__also inherited
the Hand classone line of codeso far

Note what was written.

Why: The Hand class so far is a header and a docstring — no methods at all.

Note what works.

Why: The other methods are inherited from Deck, so we can use pop_card and add_card to deal a card.

Note that printing works too.

Why: Deck.__str__ loops over self.cards, which a Hand also has, so it needs no change.

Figure (svg): The state of the program after each line of Worked example what a Hand can already do, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

A usable class from two lines. That is the code reuse inheritance offers: everything the parent could do, without repeating any of it.

Verify: Ask why __str__ works on a Hand at all.

Why: Because it only uses self.cards, which every Hand has — the method never mentions the class it was defined in. Any inherited method works exactly as far as the child provides what the method uses, which is a useful thing to check before relying on one.

9. Predict: does this work?

Prediction

Hand defines no methods of its own.

class Hand(Deck):
    """Represents a hand of playing cards."""

hand = Hand()
hand.add_card(card)
PartWhere it comes fromNote
add_cardnot defined on Handinherited from Deck
self.cardsthe Hand has onefrom the inherited __init__
the callworks

Predict first

Does hand.add_card(card) work?

  • Yes — add_card is inherited from Deck
  • No — Hand defines no methods
  • No — add_card only works on Decks
  • Only if Hand defines __init__

Correct: Yes — add_card is inherited from Deck, so it can be used for Hands as well as Decks.

Why: That is what the parentheses in the class header declare: Hand inherits from Deck, which means we can use methods like pop_card and add_card for Hands. The method works because it only uses self.cards, which the inherited __init__ has created — though as the next idea shows, it has created the wrong contents.

10. Worked example: the vocabulary

Worked example

Two words, and the relationship they name.

class Hand(Deck):

# Deck is the PARENT
# Hand is the CHILD
# Hand inherits from Deck
TermWhich classNote
the parentthe existing classDeck
the childthe new oneHand
the directionthe child inheritsnot the other way

Name the two.

Why: When a new class inherits from an existing one, the existing one is called the parent and the new class is called the child.

Note the direction.

Why: The child gets the parent's methods; nothing flows the other way, so adding a method to Hand does not give Decks that method.

Note where it is written.

Why: In the child's header, which is the only place the relationship appears.

Figure (svg): A flowchart showing methods flowing from parent to child but not back

The child gets everything the parent has; the parent gets nothing from the child.

Parent and child, with everything flowing downward. The relationship is declared once, in the parentheses after the child's name.

Verify: Check the direction with a method on the child.

Why: Adding a scoring method to Hand leaves Deck without it, so a Deck cannot be scored — which confirms that inheritance is one-way. That is the useful property: a child can add anything without any risk of disturbing the parent or its other children.

11. Trap: expecting the parent to gain the child's methods

Trap

The trap

A method is added to Hand and then called on a Deck.

Treat the two classes as related in both directions

Why: They are related, after all.

Inheritance runs one way: the child gets the parent's methods and the parent gets nothing. The call raises AttributeError, naming a method that plainly exists somewhere in the program.

The fix

Put a method where every class that needs it will get it.

On the parent, if both need it

Why: Which is why move_cards is defined on Deck.

On the child, if only it does

Why: Scoring a hand belongs on Hand.

The question to ask is which classes should have the operation. Putting it on the parent gives it to everything; putting it on the child gives it to that branch only, and there is no way to give it upward.

12. Complete it: inherit from Deck

Faded example

The parent goes in parentheses.

Fill in the blanks

class Hand(Deck):
"""Represents a hand of playing cards."""

Why: To define a new class that inherits from an existing one, you put the name of the existing class in parentheses — so Deck is the parent and Hand the child. Every method Deck defines becomes available on Hands, without being written again.

13. Discriminate: parent, child, or neither?

Discrimination

Inheritance runs one way.

Sort into buckets

Given class Hand(Deck), which is true of each statement?

true
a Hand can use add_card; a Hand can use __str__; a Hand can use shuffle; a Deck can use a method defined on Deck
false
a Deck can use a method defined on Hand; a Deck gains Hand's label attribute
yes
Each is a method flowing from parent to child, or a class using its own method — both of which inheritance provides.
no
Each expects something to flow upward, from the child to the parent. Inheritance is one-way: the parent gains nothing from its children.

14. Think it through: why is Hand the child and not Deck?

Socratic

Both hold cards.

Discussion prompt

A deck and a hand are both collections of cards. Why does Hand inherit from Deck rather than the other way round?

Hint: Which one has the operations the other needs?

Answer:

Because Deck already had the operations both need — adding, removing, printing, shuffling — so making Hand the child means writing none of them again.

Reversing it would mean Deck inherited from Hand, which would require Hand to hold all the general operations and would leave the specific ones in the wrong place.

It is worth noticing that neither is obviously more fundamental than the other — you could argue for a shared parent that both inherit from. The book's arrangement is the pragmatic one: the class that existed first became the parent, which is honest and not the only defensible design.

15. Overriding a method

Section

Section 2

16. The child's version wins

Concept

Hand inherits __init__ from Deck, but it doesn't really do what we want: instead of populating the hand with 52 new cards, the init method for Hands should initialise cards with an empty list.

override — To replace a default. Examples include replacing a default parameter with a particular argument and replacing an inherited method by providing a new method with the same name.

# inside class Hand:
    def __init__(self, label=''):
        self.cards = []
        self.label = label

>>> hand = Hand('new hand')
>>> hand.cards
[]
>>> hand.label
'new hand'
PartWhat happensNote
the child's __init__an empty listand a label
the parent'snot runit is overridden
everything elsestill inheritedunchanged

If we provide an init method in the Hand class, it overrides the one in the Deck class. When you create a Hand, Python invokes this init method, not the one in Deck.

Think Python, 2nd edition — Allen B. Downey §18.7-18.10, pp. 176-176

17. Picture it: which version runs

Picture it

The child's, if it has one.

Figure (svg): A flowchart showing method lookup finding the child's version before the parent's

The child is searched first, which is what makes overriding work.

That search order has a name — the method resolution order — and it is what find_defining_class reports.

18. Worked example: why the inherited init is wrong

Worked example

It works, and it does the wrong thing.

# Deck's __init__ builds 52 cards.
# Without an override, Hand() would too:
#   hand = Hand()
#   len(hand.cards)   ->  52

# which is a full deck, not an empty hand
AspectWhat happensNote
the inherited versionruns successfullyno error
what it producesa hand of 52 cardswrong
the fixoverride itan empty list

Note that it runs.

Why: Hand inherits __init__ from Deck, and nothing goes wrong mechanically — the nested loop builds fifty-two cards into self.cards.

Note that it is wrong.

Why: It doesn't really do what we want: a new hand should be empty.

Note the fix.

Why: Instead of populating the hand with 52 new cards, the init method for Hands should initialise cards with an empty list.

Figure (svg): The state of the program after each line of Worked example why the inherited init is wrong, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

An inherited method that runs correctly and means the wrong thing. That is the characteristic risk of inheritance: you get everything, including what you did not want.

Verify: Ask what would have happened without noticing.

Why: Dealing into the hand would work and the hand would already contain a full deck, so counts and comparisons would be silently wrong. Nothing raises, which is why the override is a correctness fix rather than a convenience.

19. Predict: how many cards in a new hand?

Prediction

Hand overrides __init__.

    def __init__(self, label=''):
        self.cards = []
        self.label = label

hand = Hand('new hand')
print(len(hand.cards))
StepWhat happensResult
Hand's __init__runsnot Deck's
self.cardsan empty list0
the parent's versionnever invokedoverridden

Predict first

What does this print?

  • 0
  • 52
  • 1
  • An error, since Hand has no cards attribute

Correct: 0 — when you create a Hand, Python invokes this init method, not the one in Deck.

Why: The override replaces the parent's version entirely, so the nested loop never runs. Without the override the answer would be 52 — a new hand containing a full deck, which is wrong and raises nothing, which is exactly why the override matters.

20. Worked example: adding an attribute the parent lacks

Worked example

The override does two things.

    def __init__(self, label=''):
        self.cards = []
        self.label = label

>>> hand = Hand('new hand')
>>> hand.label
'new hand'
LineWhat it doesNote
self.cardsreplaces the parent's contentsempty
self.labelnewDecks have no label
the defaultan empty stringso Hand() works

Replace the contents.

Why: An empty list rather than fifty-two cards, which is the reason for the override.

Add the new attribute.

Why: A label, which Decks do not have — so the child has an attribute its parent lacks.

Give it a default.

Why: So that Hand() works without arguments, following chapter 17's pattern.

Figure (svg): Two columns comparing what the parent's init method produces with the child's

Same attribute name, different contents — which is why the inherited methods still work.

A child with different contents and one extra attribute. Both are ordinary consequences of writing a different __init__.

Verify: Ask whether Deck's methods still work on a Hand.

Why: Yes — they use self.cards, which the override still provides. An inherited method breaks only if the override removes something it depends on, which is a good reason to keep the same attribute names the parent uses.

21. Trap: an override that removes what inherited methods need

Trap

The trap

A Hand's __init__ stores its cards in an attribute called hand_cards instead of cards.

Name the attribute after the class

Why: It is a hand's cards, so hand_cards reads well.

Every inherited method uses self.cards, so add_card, __str__ and shuffle all fail with AttributeError — on methods that were working a moment ago and are not shown in the child at all.

The fix

Keep the names the inherited methods use.

self.cards = []

Why: Which is what Deck's methods expect.

And add new attributes alongside

Why: self.label is fine, because nothing inherited depends on its absence.

An override replaces a method and inherits an obligation: whatever the parent's other methods rely on must still be there. That dependency is invisible in the child, which is one of the ways inheritance makes programs difficult to read.

22. Sort: inherited or overridden?

Sorting

Hand defines only __init__.

Sort into buckets

For each method called on a Hand, whose version runs?

Hand's own
__init__; a hand-scoring method
inherited from Deck
add_card; pop_card; __str__; shuffle
hand
Each is defined on Hand — one overriding the parent's version and one that the parent does not have at all.
deck
None is defined on Hand, so the search continues to the parent and finds Deck's version. They work because they only use self.cards, which Hand provides.

23. Complete it: override the init method

Faded example

A hand starts empty.

Fill in the blanks

def __init__(self, label=''):
self.cards = []
self.label = label

Why: The inherited __init__ builds fifty-two cards, which is right for a deck and wrong for a hand — so the override initialises cards with an empty list. Keeping the attribute name is essential: every inherited method uses self.cards, and renaming it would break all of them.

24. Explain it: which __init__ runs?

Explain it

Two classes define one.

Discussion prompt

A classmate asks how Python decides between Deck's __init__ and Hand's when a Hand is created. Explain.

Hint: Where does the search start?

Answer:

It searches the child first. If we provide an init method in the Hand class, it overrides the one in the Deck class — so creating a Hand invokes Hand's version and never reaches Deck's.

The parent's version is not modified or removed; it is simply not found, because a matching method was found earlier in the search.

That search has a name — the method resolution order — and there is a function in the debugging section that reports it: find_defining_class takes an object and a method name and returns the class that provides the definition.

25. move_cards, and where inheritance pays

Section

Section 3

26. One method that works on both classes

Concept

A natural next step is to encapsulate the dealing code in a method called move_cards, defined on Deck.

# inside class Deck:
    def move_cards(self, hand, num):
        for i in range(num):
            hand.add_card(self.pop_card())
ParameterWhat it can beNote
selfthe sourcea Deck or a Hand
handthe destinationalso either
numhow many to move

move_cards takes two arguments, a Hand object and the number of cards to deal. It modifies both self and hand, and returns None. And crucially: self can be either a Deck or a Hand, and hand, despite the name, can also be a Deck.

Think Python, 2nd edition — Allen B. Downey §18.7-18.10, pp. 177-177

27. Picture it: four combinations from one method

Picture it

Both parameters accept either class.

Figure (svg): The four source-and-destination combinations move_cards supports

In some games cards are moved from one hand to another, or from a hand back to the deck.

You can use move_cards for any of these operations, from one definition — which is inheritance producing polymorphism.

28. Worked example: why it works on both

Worked example

The method never asks what it has.

    def move_cards(self, hand, num):
        for i in range(num):
            hand.add_card(self.pop_card())

# it uses only:
#   self.pop_card()   - Deck's, inherited by Hand
#   hand.add_card()   - likewise
OperationAvailable onNote
pop_cardboth classes have itinherited
add_cardlikewise
no type checkanywhereso both work

List what the method uses.

Why: Two methods, both defined on Deck and both inherited by Hand.

Note the absence of a type check.

Why: Nothing asks whether self or hand is a Deck or a Hand, so both work wherever the operations do.

Recognise the rule.

Why: Chapter 17's: if all of the operations inside a function work with a given type, the function works with that type.

Figure (svg): A pipeline showing one method serving both source and destination classes

Written for Decks, and it works on anything that inherited the two methods.

Four combinations from one definition, because the method depends on operations rather than on types. Inheritance supplied the operations, and polymorphism did the rest.

Verify: Ask what would break the generality.

Why: A line like len(self.cards) == 52, or anything assuming a full deck — which would work on Decks and be meaningless on Hands. The generality is a property of what the method touches, so it survives only as long as nothing type-specific is added.

29. Predict: can a Hand be the source?

Prediction

move_cards is defined on Deck.

hand.move_cards(deck, 5)
# moving five cards from a hand back to a deck
PartWhat it isNote
move_cardsinherited by Handfrom Deck
selfthe handwhich has pop_card
hand parameterthe deckwhich has add_card

Predict first

Does this work?

  • Yes — self can be a Hand and the destination can be a Deck
  • No — move_cards only works from a Deck
  • No — the second argument must be a Hand
  • Only if Hand overrides move_cards

Correct: Yes — self can be either a Deck or a Hand, and hand, despite the name, can also be a Deck.

Why: The method uses only pop_card and add_card, which both classes have — one by definition and one by inheritance. Nothing checks types, so all four source-and-destination combinations work from a single definition. The parameter's name is the only thing suggesting otherwise.

30. Worked example: the parameter's misleading name

Worked example

The book flags it explicitly.

    def move_cards(self, hand, num):

# 'hand, despite the name, can also be a Deck'

# a more honest name might be:
#   def move_cards(self, destination, num):
AspectWhat is trueNote
the name handsuggests a Handand accepts either
the actual requirementsomething with add_cardeither class
the honest namedestinationsays what it means

Note the mismatch.

Why: The parameter is called hand and the book says outright that it can also be a Deck.

Note why it matters.

Why: A name that describes a type narrows the reader's expectation to less than the method actually supports.

Note the alternative.

Why: A name describing the role — destination, or target — would be accurate for both.

Figure (svg): The state of the program after each line of Worked example the parameter's misleading name, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

A parameter name that under-describes what the method accepts. Naming by role rather than by type is what keeps a polymorphic method's generality visible.

Verify: Ask whether the name causes a bug.

Why: It causes none — the method works on Decks whatever the parameter is called. What it causes is a reader assuming otherwise and writing a second method for deck-to-deck moves, which is a cost in duplicated code rather than in wrong behaviour.

31. Trap: writing a second method for the reverse direction

Trap

The trap

A program adds return_cards to move cards from a hand back to a deck.

Write a method for the new operation

Why: The direction is different, so a new name seems right.

move_cards already does it: self can be either a Deck or a Hand, and hand can also be a Deck. The second method is a copy of the first with the arguments swapped at the call site.

The fix

Use the general method in the other direction.

hand.move_cards(deck, 5)

Why: Which returns five cards to the deck.

And name the parameter for its role

Why: So the generality is visible to the next reader.

This is chapter 17's polymorphism again: the method works with any type supporting its operations, and both classes do because one inherited them from the other. Recognising that is what stops the duplication.

32. Complete it: move cards between collections

Faded example

Take from one and give to the other.

Fill in the blanks

def move_cards(self, hand, num):
for i in range(num):
hand.add_card(self.pop_card())

Why: pop_card removes a card from self and returns it, and add_card puts it into the destination — so one line moves a card and the loop repeats it. Both methods are defined on Deck and inherited by Hand, which is why the same code works in all four directions.

33. Discriminate: which calls does move_cards support?

Discrimination

Both parameters accept either class.

Sort into buckets

For each call, does move_cards handle it?

works
deck.move_cards(hand, 5); hand.move_cards(deck, 5); hand1.move_cards(hand2, 1); deck.move_cards(other_deck, 26)
fails
deck.move_cards(5, hand); deck.move_cards('a hand', 5)
ok
Each has a source and a destination that both have pop_card and add_card, whether by definition or by inheritance — which is all the method requires.
no
One passes the arguments in the wrong order and the other passes a string, neither of which has add_card. The method would fail inside, on the first call to a method the argument does not have.

34. Explain it yourself: why is this the payoff?

Explain it to yourself

One method, four operations.

Discussion prompt

Explain why move_cards working in all four directions is the strongest argument in this chapter for inheritance.

Hint: What made both classes have the same methods?

Answer:

Because inheritance is what gave both classes the same operations. Without it, a Hand would need its own pop_card and add_card, written separately, and a general method could not assume both existed.

With it, the method is written once against operations that both classes are guaranteed to have — so it works on any combination without a single type check.

Which is inheritance producing polymorphism: the shared parent supplies the shared interface, and any function written against that interface works on every descendant. That is the code reuse the book means, and it is more than not retyping methods.

35. The costs of inheritance

Section

Section 4

36. The book's own assessment, both halves

Concept

Inheritance is a useful feature. Some programs that would be repetitive without inheritance can be written more elegantly with it, and it can facilitate code reuse, since you can customise the behaviour of parent classes without having to modify them.

That last sentence is unusually blunt for a chapter introducing a feature, and it is worth taking seriously rather than reading as modesty.

Think Python, 2nd edition — Allen B. Downey §18.7-18.10, pp. 177-177

37. Picture it: the same feature, from both sides

Picture it

Every benefit has a matching cost.

Figure (svg): Two columns setting the benefits of inheritance against its costs

The book lists both, in the section that introduces the feature.

Which is why the next section provides a function whose whole purpose is finding where a method came from.

38. Worked example: finding where a method is defined

Worked example

The debugging section's answer to its own criticism.

def find_defining_class(obj, meth_name):
    for ty in type(obj).mro():
        if meth_name in ty.__dict__:
            return ty

>>> hand = Hand()
>>> find_defining_class(hand, 'shuffle')
<class '__main__.Deck'>
PartWhat it doesNote
type(obj).mro()the search orderthe classes Python looks through
ty.__dict__what that class defineschecked in order
the first matchthe class that provides itDeck

Note the problem it solves.

Why: When you invoke a method on an object, it might be hard to figure out which method will be invoked.

Note how it works.

Why: It uses the mro method to get the list of class objects that will be searched for methods.

Note the name.

Why: MRO stands for method resolution order, which is the sequence of classes Python searches to resolve a method name.

Figure (svg): A flowchart showing the method resolution order searched in sequence

The sequence of classes Python searches to resolve a method name.

The class that actually provides the method — Deck, for a Hand's shuffle. A function that exists because the feature makes this genuinely hard to see.

Verify: Try it on the overridden method.

Why: find_defining_class(hand, '__init__') returns Hand rather than Deck, because the child is searched first and provides its own. That contrast is what makes the tool useful: it distinguishes inherited from overridden without reading either class.

39. Two truths and a lie: inheritance

Two truths and a lie

Two are true. Keep the lie.

Eliminate the wrong options

Rule out the two true statements.

  • A. Inheritance can make programs difficult to read, since it may be unclear where a method is defined
  • B. Many of the things that can be done using inheritance can be done as well or better without it
  • C. A child class can modify the behaviour of its parent

Survives elimination: C

Why: C reverses the direction. You can customise the behaviour of parent classes without having to modify them — the child overrides a method for itself, and the parent and its other children are untouched. Nothing a child does affects the parent, which is exactly what makes inheritance safe to use.

40. Worked example: the Liskov substitution principle

Worked example

A design rule, with a memorable consequence for breaking it.

# when you override a method, the interface of the
# new method should be the same as the old:
#   the same parameters
#   the same return type
#   the same preconditions and postconditions
AspectWhat followsNote
following itany function taking a Deck works on a Handsubstitutable
violating ita function written for the parent breakson the child
the namethe Liskov substitution principle

State the rule.

Why: When you override a method, the interface of the new method should be the same as the old — the same parameters, the same return type, and the same preconditions and postconditions.

Note what it buys.

Why: If you follow this rule, any function designed to work with an instance of a parent class will also work with instances of child classes.

Note the consequence of breaking it.

Why: The book's own warning: your code will collapse like — sorry — a house of cards.

Figure (svg): The state of the program after each line of Worked example the Liskov substitution principle, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

A rule that makes children substitutable for parents. Following it is what makes move_cards work on Hands at all.

Verify: Check that Hand's override follows it.

Why: Hand.__init__ takes different parameters from Deck's, which technically departs from the rule — and it is safe because nothing calls __init__ on an arbitrary instance. That is worth noticing: the principle applies to methods that a function taking the parent might call, and construction is not one of them.

41. Trap: inheriting because two classes are similar

Trap

The trap

Two classes share several methods, so one is made to inherit from the other.

Avoid repeating the shared methods

Why: Which is a real benefit and the usual motivation.

Similar is not the same as is a kind of. If the child is not substitutable for the parent, every function written for the parent becomes a hazard — and the inheritance has bought some shared code at the cost of a false claim.

The fix

Inherit when the child is genuinely a kind of the parent.

A Hand is a kind of collection of cards

Why: Which is why the inherited operations all make sense.

Otherwise share the code another way

Why: A common function, or one class holding an instance of the other.

The book says outright that many of the things that can be done using inheritance can be done as well or better without it. Sharing code is not by itself a reason; the IS-A relationship is.

42. Predict: which class defines it?

Prediction

Hand overrides only __init__.

>>> hand = Hand()
>>> find_defining_class(hand, 'shuffle')
ClassDefines shuffle?Note
the searchHand firstthen Deck
Handdoes not define shufflecontinue
Deckdoesreturn it

Predict first

What does this return?

  • <class '__main__.Deck'>
  • <class '__main__.Hand'>
  • None
  • An AttributeError

Correct: <class '__main__.Deck'> — so the shuffle method for this Hand is the one in Deck.

Why: The function walks the method resolution order and returns the first class whose dictionary contains the name. Hand does not define shuffle, so the search continues to Deck and finds it there — which is the answer to the book's own complaint that it can be hard to figure out which method will be invoked.

43. Sort: a benefit or a cost?

Sorting

The book lists both.

Sort into buckets

For each statement about inheritance, which is it?

a benefit
customising a parent's behaviour without modifying it; programs that would be repetitive can be written more elegantly; the structure can reflect the problem's natural structure
a cost
the relevant code may be spread across several modules; it may be unclear where a method is defined; much of it can be done as well or better without it
ben
Each is a reason the book gives for using inheritance: reuse without modification, less repetition, and a design that mirrors the problem.
cost
Each is a reason the book gives against it, in the same section — difficulty reading, scattered code, and the observation that it is often unnecessary.

44. Where indirection costs readability

Real world

A benefit and a cost that are the same property.

Discussion prompt

Think of a system where behaviour is defined somewhere other than where it appears to happen. What makes it powerful, and what makes it hard?

Hint: Settings, defaults, templates.

Answer:

Defaults inherited from a parent configuration, styles applied from a stylesheet, templates filling in a page — each lets you change many things in one place, which is genuinely powerful.

And each makes the question why is it doing that? hard to answer, because the cause is somewhere the effect does not mention. You have to know the search order to find it.

Which is exactly the book's complaint about inheritance and exactly its benefit — the same property from two sides. That is why the debugging section offers a tool for reporting where a method actually came from.

45. Class diagrams and data encapsulation

Section

Section 5

46. Three relationships, and a way to find classes

Concept

A class diagram is a more abstract representation of the structure of a program. Instead of showing individual objects, it shows classes and the relationships between them.

The star near an arrow head is a multiplicity: it indicates how many Cards a Deck has. A multiplicity can be a simple number like 52, a range like 5..7, or a star, which indicates any number.

Think Python, 2nd edition — Allen B. Downey §18.7-18.10, pp. 177-178

47. Picture it: figure 18.2

Picture it

Three classes, two relationships.

Figure (svg): The class diagram's relationships between Hand, Deck and Card

A class diagram shows classes and the relationships between them rather than individual objects.

And a detail the book notes: built-in types like list and dict are usually not included, even though a Deck actually contains a list of Cards.

48. Worked example: which relationship is which

Worked example

Containment against inheritance.

class Hand(Deck):        # IS-A: a Hand is a kind of Deck

class Deck:
    def __init__(self):
        self.cards = []  # HAS-A: a Deck has Cards
What you see in codeThe relationshipNote
the parenthesesIS-Ainheritance
an attribute holding objectsHAS-Acontainment
taking one as a parametera dependencythe third kind

Spot IS-A in the header.

Why: One class might inherit from another, and this relationship is called IS-A, as in a Hand is a kind of a Deck.

Spot HAS-A in the attributes.

Why: Objects in one class might contain references to objects in another — each Rectangle contains a reference to a Point, and each Deck contains references to many Cards.

Spot a dependency in the signatures.

Why: One class might depend on another in the sense that objects in one class take objects in the second as parameters, or use them as part of a computation.

Note how they differ.

Why: IS-A is about what a class is; HAS-A about what it holds; a dependency about what it uses.

Figure (svg): Two columns distinguishing the IS-A relationship from the HAS-A relationship

Two relationships, two arrow heads, and two quite different claims.

Three relationships, each visible in a different part of the code. The diagram abstracts them so the structure can be seen without reading every class.

Verify: Check the diagram against the code for a mistake people make.

Why: A Hand IS-A Deck and HAS-A collection of Cards — both at once, which is why the diagram needs two kinds of arrow. Confusing them produces a design where a Hand contains a Deck, which would give it a deck's fifty-two cards inside a hand.

49. Discriminate: IS-A or HAS-A?

Discrimination

Two relationships, two arrow heads.

Sort into buckets

For each pair, which relationship is it?

IS-A
a Hand and a Deck; a PokerHand and a Hand; a BridgeHand and a Hand
HAS-A
a Deck and its Cards; a Rectangle and its corner Point; a Markov object and its suffix map
isa
Each is an inheritance relationship — one class is a kind of another, declared by putting the parent in parentheses.
hasa
Each is containment: an object holds a reference to another as an attribute, which is what a standard arrow head means in a class diagram.

50. Worked example: data encapsulation

Worked example

A development plan for when the objects are not obvious.

# before: globals, read and written by several functions
suffix_map = {}
prefix = ()

# after: the state of each analysis, in an object
class Markov:
    def __init__(self):
        self.suffix_map = {}
        self.prefix = ()
VersionWhat it allowsNote
the globalsone analysis at a timeshared by everything
the classone object per analysiskept separate
the functionsbecome methodstaking self

Note the problem.

Why: Because these variables are global, we can only run one analysis at a time — reading two texts would add their prefixes and suffixes to the same data structures.

Note the fix.

Why: To run multiple analyses and keep them separate, we can encapsulate the state of each analysis in an object.

Note the method.

Why: In the same way that we discovered function interfaces by encapsulation and generalisation, we can discover class interfaces by data encapsulation.

Figure (svg): The state of the program after each line of Worked example data encapsulation, drawn as a ladder with one rung per traced line

The whole run at once: each drop is one line of the program.

A class discovered from the data rather than from the problem domain. There is no Markov object in the world; the class exists because the state needed a home.

Verify: Check what the transformation actually did.

Why: The globals became instance attributes and the functions became methods taking self — mechanically the same move as chapter 17's, applied for a different reason. The reason is what is new: not that the operations belong to a type, but that the state needed to be separable.

51. Trap: expecting every class to model something real

Trap

The trap

A developer looks for real-world objects to model and cannot find one for a program's state.

Identify objects, as the previous chapters did

Why: Point, Rectangle and Time all corresponded to something.

Sometimes it is less obvious what objects you need and how they should interact. A Markov analysis is not a thing in the world, and waiting to find one means leaving the state in globals.

The fix

Let the data suggest the class.

Find state that several functions share

Why: Which is what the globals were.

Encapsulate it, and turn the functions into methods

Why: Which is data encapsulation.

The book presents this as a different development plan, needed exactly when the object-oriented design plan does not apply. Neither is the general case, and knowing both is what lets you use whichever fits.

52. Predict: what does the star mean?

Prediction

Near the arrow head from Deck to Card.

# Deck ---*---> Card
MultiplicityWhat it meansNote
a numberexactly that many52
a rangebetween two counts5..7
a starany number

Predict first

What does the star near the arrow head indicate?

  • A multiplicity — a Deck can have any number of Cards
  • That the relationship is inheritance
  • That the Card class is abstract
  • That the relationship is optional

Correct: A multiplicity — it indicates how many Cards a Deck has, and a star means any number.

Why: A multiplicity can be a simple number like 52, a range like 5..7, or a star. A star is used here rather than 52 because a Deck's card count changes as it is dealt from — and because Hand inherits the same structure with a quite different count.

53. Complete it: encapsulate the state

Faded example

The globals become attributes.

Fill in the blanks

class Markov:
def __init__(self):
self.suffix_map = ___
self.prefix = ()

Why: The globals become instance attributes, so each Markov object holds the state of one analysis and several can run without interfering. That is data encapsulation: discovering a class interface from state that several functions shared, rather than from an object in the world.

54. Explain it: when do I make a class?

Explain it

Two different answers, for two situations.

Discussion prompt

A classmate asks how to know when a program needs a class. Give them the two development plans this chapter names.

Hint: One starts from the world and one from the data.

Answer:

The first is object-oriented design: identify the objects you need — Point, Rectangle, Time — where there is an obvious correspondence between the object and some entity in the real world or a mathematical one.

The second is for when that fails. Sometimes it is less obvious what objects you need, and then you look for state that several functions share — globals, usually — and encapsulate it. The Markov class exists because two analyses could not run at once, not because a Markov analysis is a thing.

So the question to ask is which situation you are in. If an object suggests itself, model it; if not, look at what the functions are sharing, because that is usually the class waiting to be found.

55. Compare: IS-A and HAS-A

Comparison

Fill the blanks. Two relationships, two claims.

Comparison matrix

QuestionIS-AHAS-A
What is it?inheritancecontainment
How does it appear in code?the parent in parentheses in the headeran attribute holding another object
An example herea Hand is a kind of Decka Deck has Cards
Which arrow head?a hollow trianglea standard arrow

A Hand is both at once: it IS-A Deck and it HAS-A collection of Cards — which is why the diagram needs two kinds of arrow.

56. The procedure: defining a child class

Pattern

Six steps, and the third is where most mistakes happen.

  1. Check the relationship is genuinely IS-A — the child is a kind of the parent, not merely similar to it.
  2. Put the parent's name in parentheses after the child's, which is the whole declaration.
  3. Override only what differs, keeping the attribute names the inherited methods depend on.
  4. Keep an overridden method's interface the same as the old one, so the child stays substitutable.
  5. Add the operations that make sense only for the child.
  6. Use find_defining_class when you cannot tell which version of a method is running.

Step 3's warning is the one that bites: an override that renames an attribute leaves every inherited method broken, and none of them appears in the child's code.

Python documentation — Classes Classes

57. Check yourself 1 of 3: overriding

Check

Hand defines its own __init__.

class Hand(Deck):
    def __init__(self, label=''):
        self.cards = []
        self.label = label

hand = Hand()
MethodRuns?Note
Hand's __init__runsthe child is searched first
Deck'snot runoverridden
self.cardsempty

Check your understanding

Which init method runs?

  • A. Hand's — it overrides the one in Deck (correct)
  • B. Deck's, then Hand's
  • C. Deck's only
  • D. Neither, since Hand inherits

Answer: A

Why: If we provide an init method in the Hand class, it overrides the one in the Deck class — so when you create a Hand, Python invokes this init method, not the one in Deck. The parent's version is not run at all, which is why the new hand is empty rather than holding fifty-two cards.

Why B tempts people
Nothing chains them automatically. Calling the parent's version would require doing so explicitly.
Why C tempts people
This is what would happen without the override, and it would give a hand of 52 cards.
Why D tempts people
Inheriting means the parent's methods are available; defining one on the child replaces it.

58. Check yourself 2 of 3: move_cards

Check

It is defined on Deck.

Check your understanding

Which of these calls does move_cards support?

  • A. All of deck-to-hand, hand-to-hand, hand-to-deck and deck-to-deck (correct)
  • B. Only deck-to-hand
  • C. Only calls where the destination is a Hand
  • D. Only calls where the source is a Deck

Answer: A

Why: self can be either a Deck or a Hand, and hand, despite the name, can also be a Deck — because the method uses only pop_card and add_card, which both classes have. That is inheritance producing polymorphism, and it is the chapter's strongest argument for the feature.

Why B tempts people
The parameter's name suggests this and the code does not require it.
Why C tempts people
The destination needs only add_card, which a Deck has too.
Why D tempts people
The source needs only pop_card, which a Hand inherits.

59. Check yourself 3 of 3: the relationships

Check

Two arrows in figure 18.2.

Check your understanding

Which relationship does the hollow triangle arrow head represent?

  • A. IS-A — Hand inherits from Deck (correct)
  • B. HAS-A — a Deck contains Cards
  • C. A dependency
  • D. A multiplicity

Answer: A

Why: The arrow with a hollow triangle head represents an IS-A relationship, indicating inheritance; the standard arrow head represents HAS-A, containment. Dependencies would be shown with a dashed arrow, and there are none in this diagram.

Why B tempts people
That is the standard arrow head, from Deck to Card.
Why C tempts people
Dependencies are drawn with a dashed arrow, and are sometimes omitted entirely.
Why D tempts people
A multiplicity is the star near an arrow head, indicating how many.

60. Where this shows up outside this course

Real world

Defining something as a modified version of something else.

Discussion prompt

Think of a form, a template or a role defined as the standard one, but with these differences. What does that arrangement make easy, and what makes it hard to answer questions about?

Hint: Where is a particular rule actually set?

Answer:

It makes changes to the standard version propagate everywhere, which is the whole point — one edit, and every variant follows.

What it makes hard is answering why does this one behave that way?, because the answer may be in the base version, in the variant, or in something the variant inherits from further up.

Which is exactly the book's assessment: inheritance can facilitate code reuse and reflect the problem's structure, and it can make programs difficult to read, since when a method is invoked it is sometimes not clear where to find its definition.

61. Confidence wager: commit before you check

Commit first

Answer, then rate your confidence.

Predict first

move_cards is defined on Deck and takes a parameter called hand. Can you call hand.move_cards(deck, 5) to return cards to the deck?

  • Yes — self can be a Deck or a Hand, and the destination can also be either
  • No — move_cards only works with a Deck as the source
  • No — the parameter is named hand, so it must be a Hand
  • Only if Hand overrides move_cards

Correct: Yes — self can be either a Deck or a Hand, and hand, despite the name, can also be a Deck.

Why: This is the chapter's strongest argument for inheritance, and the book states it directly. The method uses only two operations — self.pop_card() and hand.add_card() — both of which are defined on Deck and therefore available on Hand. Nothing in it checks a type, so all four combinations work from one definition: dealing from a deck to a hand, passing cards between hands, returning a hand's cards to the deck, and splitting one deck into another. That is chapter 17's polymorphism arriving through inheritance: the shared parent supplies a shared interface, and any function written against that interface works on every descendant. The parameter's name is the only misleading thing about it, which is a good argument for naming parameters by their role — destination — rather than by an expected type.

62. Explain it to someone else

Explain it

A feature with a case both for and against it.

Discussion prompt

A classmate asks whether they should use inheritance for two similar classes. Give them the test and both sides of the book's assessment.

Hint: Similar is not the same as is a kind of.

Answer:

The test is whether the child is genuinely a kind of the parent — a Hand is a kind of collection of cards, so every inherited operation makes sense on it. Merely sharing some methods is not enough.

In favour: code reuse without modifying the parent, less repetition, a design reflecting the problem's structure, and functions written for the parent working on the child — which is what move_cards demonstrates.

Against, in the book's own words: it can make programs difficult to read, since it is sometimes not clear where to find a method's definition, the relevant code may be spread across several modules, and many of the things done with inheritance can be done as well or better without it.

63. Exit ticket

Exit ticket

One honest answer. It decides what the next lesson opens with.

Predict first

Which of these is still least solid for you?

  • Defining a child class, and what it inherits
  • Overriding, and which version of a method runs
  • move_cards, and why it works in all four directions
  • Class diagrams, the costs of inheritance, and data encapsulation

Correct: Whichever you picked is the right answer — this one is for you, not for a mark.

Why: The declaration is one pair of parentheses, and the thing worth being sure about is that inheritance runs one way. Overriding is mechanically simple with one real hazard: an override that renames an attribute breaks every inherited method, none of which appears in the child. move_cards is the chapter's payoff and worth being able to explain, because it shows inheritance producing polymorphism rather than merely saving typing. And the last group is the reading and design material — including the book's own list of the feature's costs, which is unusually candid and worth taking at face value.

64. Synthesis: draw the map of this lesson

Connect it up

One page, from memory.

Draw it

Draw figure 18.2: Hand, Deck and Card with the two kinds of arrow and the multiplicity, labelling which is IS-A and which HAS-A. Beside it, write the Hand class in full and mark which methods it inherits. Underneath, write move_cards and list the four combinations of source and destination it supports, with one sentence on why. Finally list two benefits and two costs of inheritance, as the book states them.

65. What you can do now

Recap

Four pages, and chapter 18 is finished: the feature most associated with object-oriented programming.

If you remember one thingIt is this
From the declarationInheritance runs one way. The parent gains nothing.
From overridingThe child is searched first, and its version replaces the parent's entirely.
From move_cardsOne method, four directions — inheritance producing polymorphism.
From the assessmentIt can make programs hard to read, and is often unnecessary.
From data encapsulationWhen no object suggests itself, look at what the functions share.

The next chapter is a tour of the features the book has deliberately postponed: conditional expressions, list comprehensions, generator expressions, any and all, sets, counters, defaultdict, named tuples, and gathering keyword arguments — the syntax that makes Python programs shorter once the fundamentals are in place.

Think Python, 2nd edition — Allen B. Downey §18.7-18.10, pp. 176-179 — everything on these slides traces back here

Sources

  1. Think Python, 2nd edition — Allen B. Downey — Allen B. Downey, Think Python: How to Think Like a Computer Scientist, 2nd edition (Green Tea Press, 2015), §18.7-18.10, pp. 176-179
  2. Python documentation — Classes
  3. Python documentation — Expressions

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

Book on Wyzant · Text (657) 465-8108