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
Title
Python · Chapter 18 — Inheritance
§18.7-18.10, pp. 176-179
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
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.
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."""| Part | What it does | Note |
|---|---|---|
| Deck in parentheses | Hand inherits from Deck | the relationship |
| what that means | pop_card and add_card work on Hands | inherited |
| the vocabulary | Deck 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
Think Python, 2nd edition — Allen B. Downey §18.7-18.10, pp. 176-176
Section
Section 1
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| Part | What happens | Note |
|---|---|---|
| the parentheses | name the parent | Deck |
| the methods | available immediately | without being written |
| what is new | nothing yet | except 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
Picture it
Every method the parent has.
Figure (svg): Two columns showing what a child class inherits and what it must define itself
Which is the case for inheritance in one picture: the shared operations are written once, and only the differences are new code.
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| Call | Where the method comes from | Note |
|---|---|---|
| hand.add_card | Deck's method | inherited |
| print(hand) | Deck's __str__ | also inherited |
| the Hand class | one line of code | so 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
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.
Prediction
Hand defines no methods of its own.
class Hand(Deck):
"""Represents a hand of playing cards."""
hand = Hand()
hand.add_card(card)| Part | Where it comes from | Note |
|---|---|---|
| add_card | not defined on Hand | inherited from Deck |
| self.cards | the Hand has one | from the inherited __init__ |
| the call | works |
Predict first
Does hand.add_card(card) work?
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.
Worked example
Two words, and the relationship they name.
class Hand(Deck):
# Deck is the PARENT
# Hand is the CHILD
# Hand inherits from Deck| Term | Which class | Note |
|---|---|---|
| the parent | the existing class | Deck |
| the child | the new one | Hand |
| the direction | the child inherits | not 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
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.
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.
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.
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.
Discrimination
Inheritance runs one way.
Sort into buckets
Given class Hand(Deck), which is true of each statement?
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.
Section
Section 2
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'| Part | What happens | Note |
|---|---|---|
| the child's __init__ | an empty list | and a label |
| the parent's | not run | it is overridden |
| everything else | still inherited | unchanged |
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
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
That search order has a name — the method resolution order — and it is what find_defining_class reports.
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| Aspect | What happens | Note |
|---|---|---|
| the inherited version | runs successfully | no error |
| what it produces | a hand of 52 cards | wrong |
| the fix | override it | an 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
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.
Prediction
Hand overrides __init__.
def __init__(self, label=''):
self.cards = []
self.label = label
hand = Hand('new hand')
print(len(hand.cards))| Step | What happens | Result |
|---|---|---|
| Hand's __init__ | runs | not Deck's |
| self.cards | an empty list | 0 |
| the parent's version | never invoked | overridden |
Predict first
What does this print?
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.
Worked example
The override does two things.
def __init__(self, label=''):
self.cards = []
self.label = label
>>> hand = Hand('new hand')
>>> hand.label
'new hand'| Line | What it does | Note |
|---|---|---|
| self.cards | replaces the parent's contents | empty |
| self.label | new | Decks have no label |
| the default | an empty string | so 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
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.
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.
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.
Sorting
Hand defines only __init__.
Sort into buckets
For each method called on a Hand, whose version runs?
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.
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.
Section
Section 3
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())| Parameter | What it can be | Note |
|---|---|---|
| self | the source | a Deck or a Hand |
| hand | the destination | also either |
| num | how 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
Picture it
Both parameters accept either class.
Figure (svg): The four source-and-destination combinations move_cards supports
You can use move_cards for any of these operations, from one definition — which is inheritance producing polymorphism.
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| Operation | Available on | Note |
|---|---|---|
| pop_card | both classes have it | inherited |
| add_card | likewise | |
| no type check | anywhere | so 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
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.
Prediction
move_cards is defined on Deck.
hand.move_cards(deck, 5)
# moving five cards from a hand back to a deck| Part | What it is | Note |
|---|---|---|
| move_cards | inherited by Hand | from Deck |
| self | the hand | which has pop_card |
| hand parameter | the deck | which has add_card |
Predict first
Does this work?
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.
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):| Aspect | What is true | Note |
|---|---|---|
| the name hand | suggests a Hand | and accepts either |
| the actual requirement | something with add_card | either class |
| the honest name | destination | says 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
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.
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.
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.
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.
Discrimination
Both parameters accept either class.
Sort into buckets
For each call, does move_cards handle it?
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.
Section
Section 4
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
Picture it
Every benefit has a matching cost.
Figure (svg): Two columns setting the benefits of inheritance against its costs
Which is why the next section provides a function whose whole purpose is finding where a method came from.
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'>| Part | What it does | Note |
|---|---|---|
| type(obj).mro() | the search order | the classes Python looks through |
| ty.__dict__ | what that class defines | checked in order |
| the first match | the class that provides it | Deck |
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 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.
Two truths and a lie
Two are true. Keep the lie.
Eliminate the wrong options
Rule out the two true statements.
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.
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| Aspect | What follows | Note |
|---|---|---|
| following it | any function taking a Deck works on a Hand | substitutable |
| violating it | a function written for the parent breaks | on the child |
| the name | the 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
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.
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.
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.
Prediction
Hand overrides only __init__.
>>> hand = Hand()
>>> find_defining_class(hand, 'shuffle')| Class | Defines shuffle? | Note |
|---|---|---|
| the search | Hand first | then Deck |
| Hand | does not define shuffle | continue |
| Deck | does | return it |
Predict first
What does this return?
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.
Sorting
The book lists both.
Sort into buckets
For each statement about inheritance, which is it?
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.
Section
Section 5
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
Picture it
Three classes, two relationships.
Figure (svg): The class diagram's relationships between Hand, Deck and Card
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.
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 code | The relationship | Note |
|---|---|---|
| the parentheses | IS-A | inheritance |
| an attribute holding objects | HAS-A | containment |
| taking one as a parameter | a dependency | the 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
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.
Discrimination
Two relationships, two arrow heads.
Sort into buckets
For each pair, which relationship is it?
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 = ()| Version | What it allows | Note |
|---|---|---|
| the globals | one analysis at a time | shared by everything |
| the class | one object per analysis | kept separate |
| the functions | become methods | taking 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
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.
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.
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.
Prediction
Near the arrow head from Deck to Card.
# Deck ---*---> Card| Multiplicity | What it means | Note |
|---|---|---|
| a number | exactly that many | 52 |
| a range | between two counts | 5..7 |
| a star | any number |
Predict first
What does the star near the arrow head indicate?
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.
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.
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.
Comparison
Fill the blanks. Two relationships, two claims.
Comparison matrix
| Question | IS-A | HAS-A |
|---|---|---|
| What is it? | inheritance | containment |
| How does it appear in code? | the parent in parentheses in the header | an attribute holding another object |
| An example here | a Hand is a kind of Deck | a Deck has Cards |
| Which arrow head? | a hollow triangle | a 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.
Pattern
Six steps, and the third is where most mistakes happen.
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
Check
Hand defines its own __init__.
class Hand(Deck):
def __init__(self, label=''):
self.cards = []
self.label = label
hand = Hand()| Method | Runs? | Note |
|---|---|---|
| Hand's __init__ | runs | the child is searched first |
| Deck's | not run | overridden |
| self.cards | empty |
Check your understanding
Which init method runs?
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.
Check
It is defined on Deck.
Check your understanding
Which of these calls does move_cards support?
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.
Check
Two arrows in figure 18.2.
Check your understanding
Which relationship does the hollow triangle arrow head represent?
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.
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.
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?
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.
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.
Exit ticket
One honest answer. It decides what the next lesson opens with.
Predict first
Which of these is still least solid for you?
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.
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.
Recap
Four pages, and chapter 18 is finished: the feature most associated with object-oriented programming.
| If you remember one thing | It is this |
|---|---|
| From the declaration | Inheritance runs one way. The parent gains nothing. |
| From overriding | The child is searched first, and its version replaces the parent's entirely. |
| From move_cards | One method, four directions — inheritance producing polymorphism. |
| From the assessment | It can make programs hard to read, and is often unnecessary. |
| From data encapsulation | When 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
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.