This lesson builds a Deck class that holds a list of cards, generates all fifty-two with a nested loop, prints them by joining a list of strings, and wraps list operations in methods appropriate for decks.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Chapter 18 — Inheritance
§18.4-18.6, pp. 174-175
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §18.4-18.6, pp. 174-175 — the pages these objectives are drawn from
Warm-up
Four suits, thirteen ranks.
Discussion prompt
You need to create every card in a standard deck. Describe the loop structure, and say how many Card objects it makes.
Hint: One loop is not enough.
Answer:
A loop over the four suits, and inside it a loop over the thirteen ranks — so the inner body runs four times thirteen, which is fifty-two.
Each pass creates one Card with the current suit and rank, and appends it to a list.
That nested-loop pattern is how you generate every combination of two things, and it is the whole of the Deck's init method.
Concept
Now that we have Cards, the next step is to define Decks. Since a deck is made up of cards, it is natural for each Deck to contain a list of cards as an attribute.
class Deck:
def __init__(self):
self.cards = []
for suit in range(4):
for rank in range(1, 14):
card = Card(suit, rank)
self.cards.append(card)| Part | What it does | Note |
|---|---|---|
| self.cards | a list attribute | the deck's contents |
| the outer loop | the suits, 0 to 3 | four passes |
| the inner loop | the ranks, 1 to 13 | thirteen each |
| the body | one Card, appended | fifty-two in total |
The easiest way to populate the deck is with a nested loop. The outer loop enumerates the suits from 0 to 3, and the inner loop enumerates the ranks from 1 to 13; each iteration creates a new Card with the current suit and rank and appends it to self.cards.
Figure (svg): A Deck object holding a list attribute containing many Card objects
Think Python, 2nd edition — Allen B. Downey §18.4-18.6, pp. 174-174
Section
Section 1
Concept
The easiest way to populate the deck is with a nested loop: the outer loop enumerates the suits and the inner loop enumerates the ranks.
self.cards = []
for suit in range(4):
for rank in range(1, 14):
card = Card(suit, rank)
self.cards.append(card)| Expression | What it produces | Note |
|---|---|---|
| range(4) | 0, 1, 2, 3 | four suits |
| range(1, 14) | 1 through 13 | thirteen ranks |
| the body | runs 4 x 13 times | 52 cards |
Note the two ranges are different shapes: range(4) starts at 0 because suit 0 is Clubs, and range(1, 14) starts at 1 because there is no card with rank zero — the same asymmetry the None place-keeper was about.
Think Python, 2nd edition — Allen B. Downey §18.4-18.6, pp. 174-174
Picture it
Four passes outside, thirteen inside each.
Figure (svg): A ladder showing the nested loop producing thirteen cards per suit
Which is why the cards come out grouped by suit: every Club appears before the first Diamond, in rank order within each.
Worked example
One starts at zero and one starts at one.
>>> list(range(4))
[0, 1, 2, 3]
>>> list(range(1, 14))
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]| Call | What it gives | Note |
|---|---|---|
| range(4) | 0 to 3 | suit 0 is Clubs |
| range(1, 14) | 1 to 13 | there is no rank 0 |
| both | exclude the upper bound | range's convention |
Note the suit range.
Why: range(4) gives 0 through 3, which is exactly the four suit codes — suit 0 is a real suit.
Note the rank range.
Why: range(1, 14) gives 1 through 13, starting at 1 because there is no card with rank zero and stopping before 14.
Note the shared convention.
Why: Both exclude the upper bound, which is why the second one ends at 14 rather than 13.
Figure (svg): The state of the program after each line of Worked example the two ranges are not the same shape, drawn as a ladder with one rung per traced line
Fifty-two combinations, from two ranges with different starting points. The asymmetry mirrors the encoding: suits are zero-based and ranks are not.
Verify: Count the result.
Why: len(deck.cards) is 52, which is four times thirteen — and it would be 56 if the rank range were range(14), including a rank-zero card whose name would be None. Counting is the check that catches an off-by-one in either range.
Prediction
Two ranges, nested.
for suit in range(4):
for rank in range(1, 14):
self.cards.append(Card(suit, rank))| Loop | How many passes | Note |
|---|---|---|
| the outer loop | 4 passes | suits 0 to 3 |
| the inner loop | 13 passes each | ranks 1 to 13 |
| the body | 4 x 13 | 52 |
Predict first
How many Cards does this create?
Correct: 52 — four suits times thirteen ranks, since the inner loop runs to completion once per outer pass.
Why: The nested loop multiplies rather than adds, which is why the count is 4 × 13 rather than 4 + 13. Option C would be the result of writing range(14) for the ranks, which would add a rank-zero card in each suit — one whose name would come back as None.
Worked example
The loop nesting decides it.
# outer loop: suits
# inner loop: ranks
# -> Ace of Clubs, 2 of Clubs, ... King of Clubs,
# Ace of Diamonds, ...
# swapping them would give:
# -> Ace of Clubs, Ace of Diamonds, Ace of Hearts, ...| Nesting | The resulting order | Note |
|---|---|---|
| suits outside | grouped by suit | all Clubs first |
| ranks outside | grouped by rank | all Aces first |
| both | the same 52 cards | in a different order |
Note which loop is outer.
Why: The suit loop, so a whole suit is generated before the next one begins.
Note what that produces.
Why: The cards come out in the order the book's printed deck shows: Ace of Clubs through King of Clubs, then the Diamonds.
Note that swapping is legal.
Why: It would produce the same fifty-two cards in a different order, which matters only until the deck is shuffled.
Figure (svg): Two columns comparing the card order produced by each loop nesting
Suit-major order, matching the printed output. The nesting is a choice, and here it happens to agree with the ordering __lt__ defines.
Verify: Compare the generated order with sorted order.
Why: A freshly built deck is already in the order sorted would produce, because both are suit-major and rank-minor. That is a coincidence of two independent decisions agreeing — and it means sorting a new deck changes nothing, which is worth knowing before using that as a test.
Trap
A student writes cards = [] as a class attribute, since every Deck needs one.
Put shared setup in the class body
Why: Which was right for suit_names in the previous lesson.
A class attribute is one object shared by every instance, so every Deck would share one list — and appending to one would fill them all. The name lists were safe to share because nothing modifies them.
Create the list in __init__.
self.cards = [] on the first line
Why: So each Deck gets its own.
The rule: share what is read, not what is modified
Why: Class attributes for constants, instance attributes for state.
This is the mutable-default trap from lesson 13b in another form: one shared mutable object where a fresh one per instance was meant. The symptom is the same — data appearing from nowhere in objects that were never touched.
Faded example
There is no card with rank zero.
Fill in the blanks
for suit in range(4):
for rank in range(1, 14):
self.cards.append(Card(suit, rank))
Why: Ranks run from 1 for the Ace to 13 for the King, and range excludes its upper bound — so range(1, 14) gives exactly those thirteen values. Writing range(14) would include rank 0, whose name in rank_names is the None place-keeper.
Discrimination
The rule is whether it is modified.
Sort into buckets
For each, where should it live?
Explain it to yourself
Two loops, and fifty-two cards.
Discussion prompt
Four suits and thirteen ranks give fifty-two cards rather than seventeen. Explain why nesting multiplies.
Hint: How many times does the inner loop run in total?
Answer:
Because the inner loop runs to completion once for every pass of the outer one — so it does not run thirteen times overall, but thirteen times per suit.
Four outer passes times thirteen inner passes each is fifty-two executions of the body, and the body creates one card.
Which is the general rule for nested loops: the body runs the product of the two counts. Two loops one after the other would add instead, and would produce seventeen values rather than fifty-two combinations.
Section
Section 2
Concept
Here is a __str__ method for Deck. This method demonstrates an efficient way to accumulate a large string: building a list of strings and then using the string method join.
# inside class Deck:
def __str__(self):
res = []
for card in self.cards:
res.append(str(card))
return '\n'.join(res)| Line | What it does | Note |
|---|---|---|
| res = [] | an accumulator | for the strings |
| str(card) | invokes Card.__str__ | one string per card |
| '\n'.join(res) | one long string | separated by newlines |
The built-in function str invokes the __str__ method on each card and returns the string representation. Since we invoke join on a newline character, the cards are separated by newlines.
Think Python, 2nd edition — Allen B. Downey §18.4-18.6, pp. 174-175
Picture it
Each card supplies its own text, and join assembles them.
Figure (svg): A pipeline showing each card converted to a string and the list joined into one
Even though the result appears on 52 lines, it is one long string that contains newlines — which is why the method returns rather than prints.
Worked example
The book calls the list-and-join approach efficient, and there is a reason.
# the list-and-join approach
res = []
for card in self.cards:
res.append(str(card))
return '\n'.join(res)
# repeated concatenation: a new string every pass
res = ''
for card in self.cards:
res = res + str(card) + '\n'| Approach | What each pass costs | Note |
|---|---|---|
| append | modifies the list | cheap |
| concatenation | builds a new string | copying everything so far |
| over 52 cards | hardly matters | over 52,000 it does |
Note what append does.
Why: It modifies the list in place, so each pass costs the same however long the list has become.
Note what concatenation does.
Why: Strings are immutable, so each pass builds a new string containing everything so far — the work grows with the length.
Note when it matters.
Why: For fifty-two cards, not at all. The pattern is worth learning because the same code on a much larger collection would be noticeably slow.
Figure (svg): A growth chart comparing list-and-join against repeated string concatenation
An efficient way to accumulate a large string. The efficiency is invisible at this size and real at a larger one, which is why the book names the technique rather than just using it.
Verify: Connect it to chapter 8's immutability.
Why: Strings cannot be modified, so res + str(card) has to create a new string every time — copying every character accumulated so far. That is the same fact that made string methods return new strings, and here it is the reason the list approach exists.
Prediction
The result appears on fifty-two lines.
deck = Deck()
s = str(deck)
# s appears on 52 lines when printed| Aspect | What is true | Note |
|---|---|---|
| the join | one string | with newlines inside |
| printing it | 52 lines | the newlines take effect |
| the value | still one string |
Predict first
What is s?
Correct: One long string containing newlines — even though the result appears on 52 lines.
Why: join produces a single string with the separator between the pieces, so the line breaks are characters inside one value rather than a structure. That is why len(s) is a character count rather than 52, and why the string can be written to a file in one operation.
Worked example
The deck's method calls the card's.
res.append(str(card))
# str(card) invokes Card.__str__
# which returns 'Ace of Clubs'
# and print(deck) invokes Deck.__str__
# which returns all 52, newline-separated| Call | Which method | Note |
|---|---|---|
| str(card) | Card.__str__ | one card's name |
| print(deck) | Deck.__str__ | the whole deck |
| the nesting | one calls the other | each class knows its own |
Note what the deck delegates.
Why: The built-in function str invokes the __str__ method on each card and returns the string representation.
Note what that buys.
Why: The Deck does not need to know how a Card is formatted — it asks, and the Card answers.
Note the layering.
Why: print(deck) invokes Deck.__str__, which invokes Card.__str__ fifty-two times. Each class is responsible for describing itself.
Figure (svg): The state of the program after each line of Worked example what str card invokes, drawn as a ladder with one rung per traced line
Two levels of the same mechanism. Changing how a card prints changes how the deck prints, with no change to the Deck class at all.
Verify: Ask what happens without Card.__str__.
Why: Each entry would be an address like <__main__.Card object at 0x...>, and the deck's output would be fifty-two of those. The Deck method would still work perfectly — which shows the delegation clearly, since the failure is entirely in the other class.
Trap
A Deck's __str__ prints each card as it goes, rather than collecting them.
Print the cards, since printing is the goal
Why: And it avoids building a list.
__str__ is supposed to return a string, so this displays the deck and then print displays None — the same failure as lesson 17b, at fifty-two lines' distance from the culprit.
Collect and return.
Append to a list, then join
Why: Which is the technique the section is demonstrating.
And let print do the printing
Why: As it was always going to.
Returning also makes the result reusable: the string can be written to a file or stored, where printing can only go to the screen. That is the same argument as lesson 17b's, and it is why __str__ has the contract it does.
Faded example
The separator goes before the join.
Fill in the blanks
res = []
for card in self.cards:
res.append(str(card))
return '\n'.join(res)
Why: join is a string method invoked on the separator, so the newline character is what comes before the dot. Since we invoke join on a newline character, the cards are separated by newlines — and the result is one long string rather than fifty-two.
Comparison
Fill the blanks. One is the technique the book is demonstrating.
Comparison matrix
| Question | Repeated concatenation | List, then join |
|---|---|---|
| What happens each pass? | a new string is built | one append to a list |
| Why? | strings are immutable | lists are mutable |
| Cost as the collection grows | grows faster than the length | grows with the length |
| Does it matter for 52 cards? | no | no — but it does for 52,000 |
The bottom row is the honest one: the technique is worth knowing before you need it, which is why the book names it here.
Explain it
The simpler code looks fine.
Discussion prompt
A classmate asks why the deck builds a list rather than adding strings together. Explain, and say when it would matter.
Hint: Strings are immutable.
Answer:
Because strings cannot be modified, so res + str(card) builds a whole new string every pass, copying everything accumulated so far. The work grows faster than the collection does.
Appending to a list modifies it in place, so each pass costs the same — and join walks the finished list once and builds the result in one go.
For fifty-two cards neither is measurable. The book names it as an efficient way to accumulate a large string because the same loop over a book's worth of lines would be noticeably slow, and the fix is the same three lines.
Section
Section 3
Concept
To deal cards, we would like a method that removes a card from the deck and returns it. The list method pop provides a convenient way to do that, and append does the reverse.
# inside class Deck:
def pop_card(self):
return self.cards.pop()
def add_card(self, card):
self.cards.append(card)| Method | What it does | Note |
|---|---|---|
| pop_card | removes and returns | the last card |
| add_card | appends | returns None |
| both | one line each | delegating to the list |
Since pop removes the last card in the list, we are dealing from the bottom of the deck — which is a detail worth noticing, since it is a consequence of using the cheapest list operation rather than a decision about card games.
Think Python, 2nd edition — Allen B. Downey §18.4-18.6, pp. 175-175
Picture it
One returns something and one does not.
Figure (svg): Two columns contrasting the deck's removing method with its adding method
Which is why the two are usually written together: deal is pop from one and add to the other, in a single line.
Worked example
The two methods compose.
>>> deck = Deck()
>>> card = deck.pop_card()
>>> hand.add_card(card)
>>> print(hand)
King of Spades| Step | What happens | Note |
|---|---|---|
| pop_card | removes the last | the King of Spades |
| add_card | puts it in the hand | returns None |
| the deck | now has 51 | one fewer |
Take a card off the deck.
Why: pop removes the last card in the list, so the King of Spades comes off first — the deck was built in suit order.
Put it in the hand.
Why: add_card appends it, and returns nothing.
Note both collections changed.
Why: The deck lost a card and the hand gained one, which is what dealing means.
Figure (svg): The state of the program after each line of Worked example dealing a card, drawn as a ladder with one rung per traced line
One card moved between two collections. The next lesson wraps this pair into a single move_cards method.
Verify: Check the counts on both sides.
Why: The deck goes from 52 to 51 and the hand from 0 to 1, so nothing was duplicated or lost. Checking both sides of a move is worth doing, because a version that appended without popping would leave the same card in two places and look correct from either side alone.
Prediction
One card is dealt.
deck = Deck()
card = deck.pop_card()
print(len(deck.cards))| Stage | What happens | Count |
|---|---|---|
| the deck | 52 cards | when built |
| pop_card | removes one | and returns it |
| the length | 51 |
Predict first
What does this print?
Correct: 51 — pop removes the card from the list as well as returning it.
Why: That is the difference between pop and indexing: deck.cards[-1] would return the same card and leave the deck at 52, which is how the same card ends up dealt twice. A method that removes and returns is exactly what dealing needs.
Worked example
It takes the last one, which is a consequence rather than a choice.
self.cards.pop() # the last card
self.cards.pop(0) # the first card
# the book: 'Since pop removes the last card
# in the list, we are dealing from the bottom
# of the deck.'| Call | Which card | Note |
|---|---|---|
| pop() | the last element | and cheap |
| pop(0) | the first element | and shifts everything |
| the choice | made by convenience | not by card-game rules |
Note the default.
Why: pop with no index removes and returns the last element, which lesson 10b covered.
Note what that means here.
Why: Since pop removes the last card in the list, we are dealing from the bottom of the deck.
Note why it does not matter.
Why: A deck is shuffled before dealing, so which end you take from is irrelevant — which is why the cheapest option is fine.
Figure (svg): A deck's card list with the last element being removed by pop
The last card, because that is what pop does by default. The book points out the consequence rather than defending it, and after shuffling it makes no difference.
Verify: Ask what pop(0) would cost.
Why: Removing the first element means shifting every other one along, so it is proportional to the deck's size rather than constant. For fifty-two cards that is nothing, and it is the reason the default exists — taking from the end is the cheap operation on a list.
Trap
A deal method calls hand.add_card(deck.cards[-1]) to give the top card to a hand.
Read the card and hand it over
Why: Indexing gets the card, which is what is needed.
Indexing reads without removing, so the card is now in the hand and still in the deck. Both collections look right individually, and the same card can be dealt twice.
Use pop, which removes and returns.
card = deck.pop_card()
Why: The deck loses it and you have it.
Then hand.add_card(card)
Why: Which completes the move.
The check that catches this is counting both sides: a move should leave the total unchanged. A duplicate is invisible from either collection on its own, which is what makes the bug survive casual testing.
Sorting
Add, remove, or read.
Sort into buckets
For each deck operation, which list method does it delegate to?
Faded example
Remove it and return it.
Fill in the blanks
def pop_card(self):
return self.cards.pop()
Why: pop removes the last element and returns it, which is exactly what a deal needs — the card leaves the deck and is available to add elsewhere. Using self.cards[-1] instead would return the card without removing it, so the same card could be dealt repeatedly.
Socratic
The book notices it and moves on.
Discussion prompt
pop takes the last card, so the deck deals from the bottom. Why does the book mention it but not fix it?
Hint: What happens before cards are dealt?
Answer:
Because the deck gets shuffled first, and after shuffling the order is random — so which end you take from makes no difference to the game.
And taking from the end is the cheap list operation: pop(0) would shift every remaining element along, which costs more for no benefit.
So it is a consequence worth noticing rather than a bug worth fixing — the kind of implementation detail that would matter if the deck were used unshuffled, and does not here. Saying so out loud is better than either hiding it or working around it.
Section
Section 4
Concept
A method like add_card that uses another method without doing much work is sometimes called a veneer. The metaphor comes from woodworking, where a veneer is a thin layer of good quality wood glued to the surface of a cheaper piece of wood to improve the appearance.
veneer — A method or function that provides a different interface to another function without doing much computation.
def add_card(self, card):
self.cards.append(card)
# the caller could write:
# deck.cards.append(card)
# but writes:
# deck.add_card(card)| Aspect | What is true | Note |
|---|---|---|
| the method | one line | delegating |
| what it adds | no computation | an interface |
| what it buys | vocabulary appropriate for decks |
add_card is a thin method that expresses a list operation in terms appropriate for decks. It improves the appearance, or interface, of the implementation.
Think Python, 2nd edition — Allen B. Downey §18.4-18.6, pp. 175-175
Picture it
One speaks about lists and one about decks.
Figure (svg): Two columns comparing calling the list method directly with calling the deck's method
Which is lesson 17b's design principle in practice: the methods a class provides should not depend on how the attributes are represented, and callers should not either.
Worked example
The implementation is free to change.
# if the Deck later stores its cards differently -
# a different structure, or with extra bookkeeping -
def add_card(self, card):
self.cards.append(card)
self.count += 1 # say
# every caller of add_card is unaffected
# every caller of deck.cards.append is not| Caller style | Effect of a change | Note |
|---|---|---|
| callers using add_card | unaffected | the interface held |
| callers using .cards.append | must all change | and some will be missed |
| the veneer | the place the change goes |
Note what the method costs now.
Why: One line, doing no computation — which is why it looks pointless.
Note what it buys later.
Why: A single place to add bookkeeping, change the structure, or validate the card.
Connect it to the principle.
Why: If you designed the interface carefully, you can change the implementation without changing the interface — lesson 17b's rule, and the veneer is how it is achieved.
Figure (svg): A flowchart showing which callers a change of implementation affects
A method that does nothing today and is the only reason a change tomorrow is cheap. That is the argument for writing it before there is anything to put in it.
Verify: Ask what the veneer cannot protect.
Why: Anything a caller reaches around it for — deck.cards is still public, so nothing stops a caller appending directly and depending on the list. The veneer offers a better interface and does not enforce one, which is the same limitation as the interface principle generally.
Prediction
It delegates to append.
def add_card(self, card):
self.cards.append(card)
x = hand.add_card(card)
print(x)| Part | What it returns | Note |
|---|---|---|
| append | modifies, returns None | lesson 10b |
| the method | no return statement | so it returns None |
| x | None |
Predict first
What does this print?
Correct: None — the method has no return statement, and append returns nothing either.
Why: add_card is a modifier: it changes the hand and returns nothing, so it should be called as a statement. Assigning its result is the same misuse as t = t.sort() from lesson 10b, and the None it stores fails on the next operation rather than here.
Worked example
The same shape, delegating to a module function.
import random
# inside class Deck:
def shuffle(self):
random.shuffle(self.cards)| Part | What it does | Note |
|---|---|---|
| random.shuffle | shuffles a list in place | a modifier |
| the method | delegates | one line |
| what it adds | deck vocabulary | deck.shuffle() |
Note the import.
Why: Don't forget to import random — the function comes from the module of lesson 13a.
Note the delegation.
Why: random.shuffle shuffles a list in place, so the method is one line and returns nothing.
Note the interface it provides.
Why: deck.shuffle() reads as an operation on a deck, where random.shuffle(deck.cards) reads as an operation on a list.
Figure (svg): The state of the program after each line of Worked example shuffle, another one-liner, drawn as a ladder with one rung per traced line
Another veneer, over a module function rather than a list method. The pattern is the same: a thin layer expressing an operation in terms appropriate for the class.
Verify: Check that it modifies rather than returns.
Why: random.shuffle shuffles in place and returns None, so the method must not return its result — writing return random.shuffle(...) would hand back None and look like a returning method. That is lesson 10c's contract question, and the method inherits its answer from the function it wraps.
Trap
A reviewer removes add_card, since callers can append to the list themselves.
Remove code that does nothing
Why: It performs no computation, so it looks like indirection for its own sake.
Every caller now depends on the deck storing a list called cards, so any change to the implementation touches all of them — which is the coupling the interface principle exists to prevent.
Recognise it as a veneer.
It improves the appearance, or interface, of the implementation
Why: Which is the book's own description.
And it is the single place a change can go
Why: Bookkeeping, validation, a different structure.
The value is not in what it computes but in what it hides. Judging a method by the work it does misses the case where the work is exactly zero and the point is the vocabulary.
Discrimination
A thin method over another operation.
Sort into buckets
For each method, is it a veneer?
Faded example
The module function does the work.
Fill in the blanks
import random
def shuffle(self):
random.shuffle(self.cards)
Why: random.shuffle rearranges a list in place, so the method delegates and returns nothing. Naming the method shuffle as well is deliberate: the veneer's job is to express the operation in terms appropriate for decks, and deck.shuffle() reads better than random.shuffle(deck.cards).
Real world
Something that adds no capability and changes the vocabulary.
Discussion prompt
Think of an interface that does nothing but present something else in more suitable terms. What does it buy?
Hint: A switch, a form, a shortcut.
Answer:
A light switch is a veneer over wiring; a form is one over a database; a shortcut is one over a longer path. None adds capability, and each speaks the language of the person using it.
What they buy is that the thing underneath can change without anyone noticing — rewire the room, change the schema, move the file. The interface holds.
Which is exactly what add_card provides. It performs no computation and it is the reason a change to how a Deck stores its cards is a one-line edit rather than a search through every caller.
Section
Section 5
Concept
The Deck class is complete after five short methods, and the set is worth noticing because most collection classes need the same ones.
Four of the five are one line, and the exceptions are the two that do real work: generating fifty-two cards, and accumulating fifty-two strings.
Think Python, 2nd edition — Allen B. Downey §18.4-18.6, pp. 174-176
Picture it
Most of a collection class is interface.
Figure (svg): The Deck class's methods listed with how much work each does
Which is normal for a class wrapping a built-in structure: the structure does the work and the class supplies the terms.
Worked example
The exercise, and it is one line for a reason.
# inside class Deck:
def sort(self):
self.cards.sort()| Part | What it does | Note |
|---|---|---|
| the list's sort | orders in place | a modifier |
| how it compares | using Card.__lt__ | defined last lesson |
| the method | a veneer | one line |
Delegate to the list.
Why: Write a Deck method named sort that uses the list method sort to sort the cards in a Deck.
Note what makes it work.
Why: sort uses the __lt__ method we defined to determine the order — so the comparison written in the previous lesson does the job.
Note the shape.
Why: Another veneer: no computation, and a name appropriate for decks.
Figure (svg): The state of the program after each line of Worked example the sort method, drawn as a ladder with one rung per traced line
One line, resting on two pieces written earlier — the list's sort and the Card's comparison. Neither knows about the other, and the method connects them.
Verify: Test it on a shuffled deck.
Why: Sorting a freshly built deck changes nothing, because it is already in suit-major order — so the test proves nothing. Shuffling first is what makes the sort observable, which is a case where the obvious test is the useless one.
Prediction
The deck was built with a nested loop.
deck = Deck()
before = str(deck)
deck.sort()
print(str(deck) == before)| Order | What determines it | Note |
|---|---|---|
| the build order | suit-major, rank-minor | from the nested loop |
| the sort order | suit first, then rank | from __lt__ |
| the two | agree | so nothing moves |
Predict first
What does this print?
Correct: True — the nested loop generates the cards in the same order __lt__ sorts them into.
Why: Two independent decisions happen to agree: the loop nests suits outside ranks, and the comparison puts suit before rank. So a fresh deck is already sorted, which makes this a useless test of the sort method — shuffling first is what would exercise it.
Worked example
Some operations are deliberately absent.
# there is no:
# deck.find(card)
# deck.remove(card) - remove a specific card
# deck.deal(n) - deal several at once
#
# move_cards comes next lesson, on the Deck class| Operation | Present? | Note |
|---|---|---|
| what exists | add one, remove the last | the minimum |
| what does not | search, remove a specific card | not needed yet |
| move_cards | the next lesson | built from these two |
Note the minimum.
Why: The class provides exactly what the chapter's examples need — adding one card and removing the last.
Note what is built on it.
Why: move_cards, in the next lesson, is a loop over pop_card and add_card, and needs nothing new.
Note the principle.
Why: Adding methods before there is a use for them means guessing at an interface, which is harder than adding them when the need appears.
Figure (svg): Two columns separating the operations the Deck provides from those it does not
A small class that does what is needed. The next lesson adds one method and gets a whole new class from inheritance.
Verify: Ask what a poker program would want next.
Why: Comparing hands, scoring them, and finding pairs — operations on a hand rather than on a deck, which is exactly why the next lesson introduces a Hand class rather than adding to this one. Noticing that the missing operations belong to a different type is what motivates inheritance.
Trap
A Deck class is given find, remove, count_suit and deal_to_all before any of them is called.
Anticipate what will be needed
Why: A complete class looks more finished.
Each is designed against a guess about how it will be used, and guesses about interfaces are usually wrong in detail — so they get rewritten when the real caller appears, having cost time twice.
Add methods when a caller needs one.
Start with what the examples use
Why: Which for this chapter is four operations.
And let the caller's shape decide the signature
Why: move_cards takes a hand and a count because that is what the calling code wanted.
This is chapter 13's advice in another form: choose the easiest thing that works and improve it when you know more. An unused method is an untested guess with a name.
Faded example
The list method uses the comparison you defined.
Fill in the blanks
def sort(self):
self.cards.sort()
Why: The Deck holds its contents in an attribute called cards, and the list's sort method orders them using Card.__lt__ — so the whole method is one line. Writing self.sort() instead would call this method again and recurse until the interpreter stops it.
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 misses what a veneer is for. add_card performs no computation and improves the appearance, or interface, of the implementation — and it is the single place a change to how a Deck stores cards can go. Removing it makes every caller depend on the list attribute.
Explain it
One line, delegating.
Discussion prompt
A classmate wants to delete add_card because it just calls append. Give them two reasons to keep it.
Hint: What does a caller depend on either way?
Answer:
First, vocabulary. deck.add_card(card) speaks about decks, where deck.cards.append(card) speaks about lists — and the class exists to provide the first kind of language.
Second, and more practically: every caller that appends directly depends on the deck storing a list called cards. Change that, and all of them break; keep the method, and the change goes in one place.
The book calls it a veneer, from woodworking — a thin layer of good quality wood over a cheaper piece, to improve the appearance. The value is not in what it computes but in what it hides.
Comparison
Fill the blanks. Most of a collection class is interface.
Comparison matrix
| Question | Methods that compute | Veneers |
|---|---|---|
| Which are they? | __init__ and __str__ | add_card, pop_card, shuffle, sort |
| How long? | several lines each | one line each |
| What do they add? | the construction and the formatting | vocabulary appropriate for decks |
| Why keep them? | nothing else does the work | they hide the implementation from callers |
The bottom row is the one worth arguing about, because a veneer looks removable until an implementation changes.
Pattern
Six steps, and the fourth is the one that looks unnecessary.
Step 1's in __init__ matters: a list created in the class body would be a class attribute, shared by every instance — so appending to one deck would fill them all.
Python documentation — Classes Classes
Check
Four suits and thirteen ranks.
for suit in range(4):
for rank in range(1, 14):
self.cards.append(Card(suit, rank))| Loop | Passes | Note |
|---|---|---|
| outer | 4 passes | |
| inner | 13 per outer pass | |
| body | 4 x 13 | 52 |
Check your understanding
How many cards does this create?
Answer: A
Why: The inner loop runs to completion once for every pass of the outer one, so the body runs four times thirteen. Nesting multiplies where two consecutive loops would add — which is what makes this the way to generate every combination of two things.
Check
The result appears on 52 lines.
Check your understanding
What does Deck.__str__ return?
Answer: A
Why: join produces a single string with the separator between the pieces, so even though the result appears on 52 lines, it is one long string that contains newlines. The list is an intermediate step — an efficient way to accumulate a large string.
Check
A method that calls append and does nothing else.
Check your understanding
What is a veneer, and why write one?
Answer: A
Why: The metaphor comes from woodworking, where a veneer is a thin layer of good quality wood glued to a cheaper piece to improve the appearance. add_card performs no computation and improves the interface of the implementation — and it is the single place a change to the storage can go.
Real world
A wrapper that adds no capability and changes the terms.
Discussion prompt
Think of a control or a form that hides something more complicated underneath. What would break if everyone bypassed it?
Hint: A switch, a booking page, a menu item.
Answer:
The thing underneath could not change. Rewiring the room, changing the database, moving the file — each is invisible while everyone goes through the control, and impossible once they do not.
Which is exactly the veneer's argument. add_card computes nothing, and it is the reason a change to how a Deck stores its cards touches one line rather than every caller.
And the limitation is the same in both cases: offering a better route does not prevent the other one. deck.cards is still reachable, so the discipline has to be kept rather than enforced.
Commit first
Answer, then rate your confidence.
Predict first
Why write add_card, when it does nothing but call the list's append?
Correct: It is a veneer: it expresses a list operation in terms appropriate for decks, and hides the implementation from callers.
Why: The book names the pattern and gives the metaphor: a veneer is a thin layer of good quality wood glued to the surface of a cheaper piece to improve the appearance, and add_card is a thin method that improves the appearance, or interface, of the implementation. The practical value is the one lesson 17b's design principle predicted. A caller writing deck.cards.append(card) depends on the deck storing a list called cards, so changing that representation means finding and fixing every such caller — and missing some. A caller writing deck.add_card(card) depends on the interface, so the change goes in one line inside the class. That is also where later additions would go: bookkeeping, validation, or a different structure entirely. The method is worth writing precisely when it looks least worth writing, because the cost of not having it appears only when something changes.
Explain it
A class that mostly delegates.
Discussion prompt
A classmate says the Deck class barely does anything — four of its methods are one line. Explain what those lines are for.
Hint: What does a caller have to know either way?
Answer:
They are veneers: each expresses a list operation in terms appropriate for decks, so callers can say what they mean rather than how it is stored.
And they are the boundary. Without them every caller depends on the deck holding a list called cards, so any change to that representation is a change everywhere — with them, it is a change in one place.
The two methods that do real work are the ones nothing else could do: generating fifty-two cards with a nested loop, and accumulating fifty-two strings efficiently. That ratio is normal for a class wrapping a built-in structure.
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 nested loop is the standard way to generate every combination of two things, and the multiplication is the part worth being certain about. The list-and-join technique matters far beyond this class, and the reason is chapter 8's immutability. The two card methods are lesson 10c's contracts with new names, and the counting check is what catches a card dealt twice. And the veneer is the idea most likely to be dismissed — a method that does nothing today and is the only reason a change tomorrow is cheap.
Connect it up
One page, from memory.
Draw it
Draw a Deck object with its cards attribute pointing at a list of Card objects. Beside it, write the nested loop and mark why it produces fifty-two. Underneath, write __str__ and label the three stages — convert, collect, join — noting what str(card) invokes. Finally list the four one-line methods and write in one sentence what a veneer is for.
Recap
Two pages, and a class ready to be inherited from.
| If you remember one thing | It is this |
|---|---|
| From the attribute | Create the list in __init__, or every instance shares one. |
| From the nested loop | Nesting multiplies. Four and thirteen give fifty-two. |
| From __str__ | Build a list and join it. Strings are immutable, so concatenating repeats work. |
| From pop and append | One returns the item and one returns None. Count both sides of a move. |
| From the veneer | It computes nothing, and it is the only place a change of storage has to go. |
The next lesson defines a Hand as a modified version of a Deck — the chapter's title finally arriving — with the parent and child vocabulary, an overridden init method, class diagrams showing IS-A and HAS-A, and the Liskov substitution principle.
Think Python, 2nd edition — Allen B. Downey §18.4-18.6, pp. 174-175 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.