18b Decks: Building, Printing, Adding, Removing, Shuffling

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

What this lesson covers

The lesson, slide by slide

1. Lesson 18b Decks: Building, Printing, Adding, Removing, Shuffling

Title

Python · Chapter 18 — Inheritance

§18.4-18.6, pp. 174-175

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.4-18.6, pp. 174-175 — the pages these objectives are drawn from

3. Before we start: fifty-two cards

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.

4. The one idea behind this lesson: a class that holds a collection

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)
PartWhat it doesNote
self.cardsa list attributethe deck's contents
the outer loopthe suits, 0 to 3four passes
the inner loopthe ranks, 1 to 13thirteen each
the bodyone Card, appendedfifty-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

A Deck has a list, and the list has Cards — which the next lesson names a HAS-A relationship.

Think Python, 2nd edition — Allen B. Downey §18.4-18.6, pp. 174-174

5. Generating the deck

Section

Section 1

6. A nested loop over two ranges

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)
ExpressionWhat it producesNote
range(4)0, 1, 2, 3four suits
range(1, 14)1 through 13thirteen ranks
the bodyruns 4 x 13 times52 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

7. Picture it: the inner loop runs once per outer pass

Picture it

Four passes outside, thirteen inside each.

Figure (svg): A ladder showing the nested loop producing thirteen cards per suit

The inner loop runs to completion once for every value of the outer one.

Which is why the cards come out grouped by suit: every Club appears before the first Diamond, in rank order within each.

8. Worked example: the two ranges are not the same shape

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]
CallWhat it givesNote
range(4)0 to 3suit 0 is Clubs
range(1, 14)1 to 13there is no rank 0
bothexclude the upper boundrange'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

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

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.

9. Predict: how many cards?

Prediction

Two ranges, nested.

for suit in range(4):
    for rank in range(1, 14):
        self.cards.append(Card(suit, rank))
LoopHow many passesNote
the outer loop4 passessuits 0 to 3
the inner loop13 passes eachranks 1 to 13
the body4 x 1352

Predict first

How many Cards does this create?

  • 52
  • 17
  • 56
  • 13

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.

10. Worked example: the order the cards come out in

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, ...
NestingThe resulting orderNote
suits outsidegrouped by suitall Clubs first
ranks outsidegrouped by rankall Aces first
boththe same 52 cardsin 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

The nesting decides the order, and both produce a complete deck.

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.

11. Trap: creating the list outside the init method

Trap

The 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.

The fix

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.

12. Complete it: the rank range

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.

13. Discriminate: class attribute or instance attribute?

Discrimination

The rule is whether it is modified.

Sort into buckets

For each, where should it live?

an instance attribute
the list of a deck's cards; a hand's label; a card's suit
a class attribute
the list of suit names; the list of rank names; the number of suits, as a constant
ins
Each differs from object to object and is modified — a deck's cards change as it is dealt from, and no two hands share a label. Sharing them would make every instance identical.
cls
Each is the same for every instance and is only read, so one shared copy is right — which is exactly what suit_names and rank_names are.

14. Explain it yourself: why does the nesting multiply?

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.

15. Printing a collection

Section

Section 2

16. Build a list of strings, then join it

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)
LineWhat it doesNote
res = []an accumulatorfor the strings
str(card)invokes Card.__str__one string per card
'\n'.join(res)one long stringseparated 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

17. Picture it: fifty-two strings become one

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

Each card knows how to describe itself; the deck only has to collect the answers.

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.

18. Worked example: why build a list rather than concatenate

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'
ApproachWhat each pass costsNote
appendmodifies the listcheap
concatenationbuilds a new stringcopying everything so far
over 52 cardshardly mattersover 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

Both are fine for a deck. Only one stays fine for a book.

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.

19. Predict: how many lines, and how many strings?

Prediction

The result appears on fifty-two lines.

deck = Deck()
s = str(deck)
# s appears on 52 lines when printed
AspectWhat is trueNote
the joinone stringwith newlines inside
printing it52 linesthe newlines take effect
the valuestill one string

Predict first

What is s?

  • One long string containing newlines
  • A list of 52 strings
  • 52 separate strings
  • None, since __str__ prints

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.

20. Worked example: what str(card) invokes

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
CallWhich methodNote
str(card)Card.__str__one card's name
print(deck)Deck.__str__the whole deck
the nestingone calls the othereach 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

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

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.

21. Trap: printing inside the loop instead of returning

Trap

The 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.

The fix

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.

22. Complete it: join the strings

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.

23. Compare: two ways to build a long string

Comparison

Fill the blanks. One is the technique the book is demonstrating.

Comparison matrix

QuestionRepeated concatenationList, then join
What happens each pass?a new string is builtone append to a list
Why?strings are immutablelists are mutable
Cost as the collection growsgrows faster than the lengthgrows with the length
Does it matter for 52 cards?nono — 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.

24. Explain it: why not just concatenate?

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.

25. Adding and removing cards

Section

Section 3

26. Methods that delegate to the list

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)
MethodWhat it doesNote
pop_cardremoves and returnsthe last card
add_cardappendsreturns None
bothone line eachdelegating 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

27. Picture it: the two contracts, again

Picture it

One returns something and one does not.

Figure (svg): Two columns contrasting the deck's removing method with its adding method

Both modify the deck; only one has a useful return value.

Which is why the two are usually written together: deal is pop from one and add to the other, in a single line.

28. Worked example: dealing a card

Worked example

The two methods compose.

>>> deck = Deck()
>>> card = deck.pop_card()
>>> hand.add_card(card)
>>> print(hand)
King of Spades
StepWhat happensNote
pop_cardremoves the lastthe King of Spades
add_cardputs it in the handreturns None
the decknow has 51one 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

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

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.

29. Predict: how many cards are left?

Prediction

One card is dealt.

deck = Deck()
card = deck.pop_card()
print(len(deck.cards))
StageWhat happensCount
the deck52 cardswhen built
pop_cardremoves oneand returns it
the length51

Predict first

What does this print?

  • 51
  • 52
  • 1
  • None

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.

30. Worked example: what pop with no argument does

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.'
CallWhich cardNote
pop()the last elementand cheap
pop(0)the first elementand shifts everything
the choicemade by conveniencenot 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 comes off, which is the cheap end of a list.

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.

31. Trap: adding without removing

Trap

The 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.

The fix

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.

32. Sort: which list method does this need?

Sorting

Add, remove, or read.

Sort into buckets

For each deck operation, which list method does it delegate to?

pop
deal a card off the deck; return the last card and remove it
append
put a card into a hand; add a card to the end
indexing or len
look at the top card without taking it; count the cards
pop
Both remove an element and return it, which is what dealing needs — the card leaves one collection and is available to put in another.
app
Both add an element to the end and return None, which is the adding half of a move.
oth
Neither changes the list: indexing reads a card and leaves it in place, and len counts without touching anything.

33. Complete it: deal a card

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.

34. Think it through: does dealing from the bottom matter?

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.

35. The veneer

Section

Section 4

36. A thin method that improves the interface

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)
AspectWhat is trueNote
the methodone linedelegating
what it addsno computationan interface
what it buysvocabulary 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

37. Picture it: the same operation, two vocabularies

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

The same computation. The right column improves the appearance, or interface, of the implementation.

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.

38. Worked example: what the veneer protects

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 styleEffect of a changeNote
callers using add_cardunaffectedthe interface held
callers using .cards.appendmust all changeand some will be missed
the veneerthe 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

The veneer is a single place for a change that would otherwise be everywhere.

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.

39. Predict: what does add_card return?

Prediction

It delegates to append.

    def add_card(self, card):
        self.cards.append(card)

x = hand.add_card(card)
print(x)
PartWhat it returnsNote
appendmodifies, returns Nonelesson 10b
the methodno return statementso it returns None
xNone

Predict first

What does this print?

  • None
  • The card
  • The hand
  • The number of cards

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.

40. Worked example: shuffle, another one-liner

Worked example

The same shape, delegating to a module function.

import random

# inside class Deck:
    def shuffle(self):
        random.shuffle(self.cards)
PartWhat it doesNote
random.shuffleshuffles a list in placea modifier
the methoddelegatesone line
what it addsdeck vocabularydeck.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

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

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.

41. Trap: dismissing a one-line method as pointless

Trap

The 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.

The fix

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.

42. Discriminate: is this a veneer?

Discrimination

A thin method over another operation.

Sort into buckets

For each method, is it a veneer?

a veneer
add_card, which calls append; shuffle, which calls random.shuffle; pop_card, which calls pop; sort, which calls the list's sort
does real work
Deck.__init__, which builds 52 cards; Deck.__str__, which loops and joins
yes
Each uses another method or function without doing much work, providing a different interface to an operation that already exists — which is exactly the definition.
no
Each does substantial work of its own: one runs a nested loop creating fifty-two objects, and the other converts and accumulates fifty-two strings.

43. Complete it: the shuffle 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).

44. Where a thin layer earns its keep

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.

45. The methods a collection class needs

Section

Section 5

46. Build, show, add, remove, rearrange

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

47. Picture it: the five methods, by size

Picture it

Most of a collection class is interface.

Figure (svg): The Deck class's methods listed with how much work each does

Two methods compute; four provide vocabulary.

Which is normal for a class wrapping a built-in structure: the structure does the work and the class supplies the terms.

48. Worked example: the sort method

Worked example

The exercise, and it is one line for a reason.

# inside class Deck:
    def sort(self):
        self.cards.sort()
PartWhat it doesNote
the list's sortorders in placea modifier
how it comparesusing Card.__lt__defined last lesson
the methoda veneerone 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

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

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.

49. Predict: does sorting a new deck change anything?

Prediction

The deck was built with a nested loop.

deck = Deck()
before = str(deck)
deck.sort()
print(str(deck) == before)
OrderWhat determines itNote
the build ordersuit-major, rank-minorfrom the nested loop
the sort ordersuit first, then rankfrom __lt__
the twoagreeso nothing moves

Predict first

What does this print?

  • True — the deck was already in sorted order
  • False — sorting always changes the order
  • An error, since Decks cannot be sorted
  • It depends on the shuffle

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.

50. Worked example: what the class does not provide

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
OperationPresent?Note
what existsadd one, remove the lastthe minimum
what does notsearch, remove a specific cardnot needed yet
move_cardsthe next lessonbuilt 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

The right-hand column is mostly operations on a hand, which is the next lesson's subject.

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.

51. Trap: adding methods before there is a use

Trap

The 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.

The fix

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.

52. Complete it: sort the deck

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.

53. Two truths and a lie: the Deck class

Two truths and a lie

Two are true. Keep the lie.

Eliminate the wrong options

Rule out the two true statements.

  • A. Since pop removes the last card, the deck deals from the bottom
  • B. Deck.__str__ returns one long string containing newlines
  • C. add_card is unnecessary, since callers can append to the list themselves

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.

54. Explain it: why write a method that does nothing?

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.

55. Compare: the two kinds of method in this class

Comparison

Fill the blanks. Most of a collection class is interface.

Comparison matrix

QuestionMethods that computeVeneers
Which are they?__init__ and __str__add_card, pop_card, shuffle, sort
How long?several lines eachone line each
What do they add?the construction and the formattingvocabulary appropriate for decks
Why keep them?nothing else does the workthey hide the implementation from callers

The bottom row is the one worth arguing about, because a veneer looks removable until an implementation changes.

56. The procedure: writing a collection class

Pattern

Six steps, and the fourth is the one that looks unnecessary.

  1. Hold the contents in an instance attribute — created in __init__, so each object has its own.
  2. Build the initial contents there, with a nested loop if you need every combination of two things.
  3. Write __str__ by collecting each element's string into a list and joining it.
  4. Add thin methods for putting things in and taking them out, named for the collection rather than the structure.
  5. Delegate rearranging to the structure's own methods, which use your comparison where one is needed.
  6. Stop there, and add more when a caller needs one.

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

57. Check yourself 1 of 3: the nested loop

Check

Four suits and thirteen ranks.

for suit in range(4):
    for rank in range(1, 14):
        self.cards.append(Card(suit, rank))
LoopPassesNote
outer4 passes
inner13 per outer pass
body4 x 1352

Check your understanding

How many cards does this create?

  • A. 52 (correct)
  • B. 17
  • C. 56
  • D. 13

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.

Why B tempts people
This is 4 + 13, which is what two loops one after another would give.
Why C tempts people
This would be 4 × 14, from writing range(14) and including a rank-zero card.
Why D tempts people
This is the inner loop's count for a single suit.

58. Check yourself 2 of 3: the string method

Check

The result appears on 52 lines.

Check your understanding

What does Deck.__str__ return?

  • A. One long string containing newline characters (correct)
  • B. A list of 52 strings
  • C. None, since it prints the cards
  • D. 52 separate strings

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.

Why B tempts people
The list is built inside the method and consumed by join; what comes back is the joined result.
Why C tempts people
__str__ returns rather than prints. A version that printed would show the deck and then None.
Why D tempts people
A function returns one value. The newlines are characters inside it, not a structure.

59. Check yourself 3 of 3: the veneer

Check

A method that calls append and does nothing else.

Check your understanding

What is a veneer, and why write one?

  • A. A thin method that expresses an operation in terms appropriate for the class, improving the interface (correct)
  • B. A method that makes the code run faster
  • C. A method that is called automatically
  • D. A method inherited from a parent class

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.

Why B tempts people
It adds a function call, so if anything it is marginally slower. The benefit is in the interface.
Why C tempts people
That describes a special method like __str__, which is a different idea.
Why D tempts people
Inheritance is the next lesson. A veneer is about delegation within one class.

60. Where this shows up outside this course

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.

61. Confidence wager: commit before you check

Commit first

Answer, then rate your confidence.

Predict first

Why write add_card, when it does nothing but call the list's append?

  • It is a veneer: it expresses a list operation in terms appropriate for decks, and hides the implementation from callers
  • It makes appending faster
  • Because Python requires a method for every attribute
  • So that add_card can be inherited

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.

62. Explain it to someone else

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.

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?

  • The nested loop, and why it produces fifty-two rather than seventeen
  • Building a string by collecting into a list and joining
  • pop_card and add_card, and the two contracts
  • The veneer, and why a one-line method is worth writing

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.

64. Synthesis: draw the map of this lesson

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.

65. What you can do now

Recap

Two pages, and a class ready to be inherited from.

If you remember one thingIt is this
From the attributeCreate the list in __init__, or every instance shares one.
From the nested loopNesting 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 appendOne returns the item and one returns None. Count both sides of a move.
From the veneerIt 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

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.4-18.6, pp. 174-175
  2. Python documentation — Classes
  3. Python documentation — random — Generate pseudo-random numbers

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

Book on Wyzant · Text (657) 465-8108