18a Card Objects, Class Attributes, and Comparing Cards

This lesson encodes playing cards as integers, distinguishes class attributes from instance attributes, and defines an ordering for cards with the __lt__ method.

Subject: Python · 65 slides · code lesson

Open the interactive version of this deck

What this lesson covers

The lesson, slide by slide

1. Lesson 18a Card Objects, Class Attributes, and Comparing Cards

Title

Python · Chapter 18 — Inheritance

§18.1-18.3, pp. 171-173

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.1-18.3, pp. 171-173 — the pages these objectives are drawn from

3. Before we start: which is better, the 3 of Clubs or the 2 of Diamonds?

Warm-up

One has a higher rank and the other a higher suit.

Discussion prompt

Decide which card is better, then say what your decision depended on — and whether the question has a right answer at all.

Hint: Does it depend on the game?

Answer:

It depends on whether rank or suit matters more, which the cards themselves do not settle. The answer might depend on what game you are playing.

So this is a decision the programmer has to make rather than discover — and once made, it has to be recorded somewhere, because nothing in the data implies it.

The book makes the arbitrary choice that suit is more important, so all the Spades outrank all the Diamonds. Noticing that it is arbitrary is the point.

4. The one idea behind this lesson: choose an encoding that makes the operations easy

Concept

If we want to define a new object to represent a playing card, it is obvious what the attributes should be: rank and suit. It is not as obvious what type the attributes should be.

encode — To represent one set of values using another set of values, by constructing a mapping between them.

This kind of encoding is not meant to be a secret — that would be encryption. And the mappings are part of the program design; they don't appear explicitly in the code.

Figure (svg): Two columns comparing string and integer encodings for a card's suit and rank

Higher suits map to higher numbers, so we can compare suits by comparing their codes.

Think Python, 2nd edition — Allen B. Downey §18.1-18.3, pp. 171-172

5. Encoding suits and ranks

Section

Section 1

6. A mapping between numbers and things

Concept

There are fifty-two cards in a deck, each of which belongs to one of four suits and one of thirteen ranks. Using integers to encode them makes it easy to compare cards, because higher suits map to higher numbers.

# Spades   -> 3
# Hearts   -> 2
# Diamonds -> 1
# Clubs    -> 0

# Jack  -> 11
# Queen -> 12
# King  -> 13
GroupThe codesNote
the suits0 to 3in ascending order for bridge
the numeric rankseach maps to itself2 is 2
the face cards11, 12, 13continuing the sequence

The book uses an arrow symbol to make it clear that these mappings are not part of the Python program — they are part of the program design, and they don't appear explicitly in the code.

Think Python, 2nd edition — Allen B. Downey §18.1-18.3, pp. 171-172

7. Picture it: the two mappings

Picture it

Four suits and thirteen ranks, each with a number.

Figure (svg): The suit and rank encodings shown as mappings from names to integers

Higher things map to higher numbers, which is what makes comparison work.

The mapping's direction is the design decision. Reversing it would make Clubs beat Spades, and nothing in the code would look wrong.

8. Worked example: the Card class

Worked example

Two attributes, both integers, with defaults.

class Card:
    """Represents a standard playing card."""

    def __init__(self, suit=0, rank=2):
        self.suit = suit
        self.rank = rank

queen_of_diamonds = Card(1, 12)
ParameterWhat the default meansNote
suit=0Clubsthe default
rank=2the 2the default
Card(1, 12)Diamonds, Queenthe queen of diamonds

Note the init method's shape.

Why: As usual, the init method takes an optional parameter for each attribute — chapter 17's pattern, unchanged.

Note the defaults.

Why: The default card is the 2 of Clubs, which is suit 0 and rank 2 under the encoding.

Create a card.

Why: To create a Card, you call Card with the suit and rank of the card you want — suit first, then rank.

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

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

A two-attribute class with sensible defaults. Note that the argument order is suit then rank, which the constructor call gives no hint about.

Verify: Check the default against the encoding.

Why: Suit 0 is Clubs and rank 2 is the 2, so Card() is the 2 of Clubs — the book's stated default. Confirming that the defaults decode to something sensible is worth a moment, since a default of rank 0 would be a card that does not exist.

9. Predict: which card is this?

Prediction

Suit first, then rank.

card = Card(1, 12)
# suit 1, rank 12
CodeWhat it meansNote
suit 1Diamondsfrom the mapping
rank 12Queenlikewise
the cardthe queen of diamonds

Predict first

Which card does this create?

  • The queen of diamonds
  • The jack of hearts
  • The 12 of hearts
  • The ace of diamonds

Correct: The queen of diamonds — suit 1 is Diamonds and rank 12 is the Queen.

Why: The argument order is suit then rank, which the call itself does not indicate — Card(1, 12) looks the same as Card(12, 1) would, and only one of them is a real card. That is the readability cost of the integer encoding, and it is the trade taken for easy comparison.

10. Worked example: why not strings

Worked example

The alternative, and the specific problem with it.

# with strings:
card.suit = 'Spades'
card.rank = 'Queen'

# comparing them:
'Spades' < 'Hearts'      # True - alphabetical, and wrong
'Queen' < 'Jack'         # False - also alphabetical
AspectWhat happensNote
strings comparealphabeticallynot by card value
Spades and HeartsH before Sso Hearts wins, wrongly
the fixa lookup table per comparisonor integers

Note the readability advantage.

Why: One possibility is to use strings containing words like 'Spade' for suits and 'Queen' for ranks, which anyone can read.

Note the problem.

Why: One problem with this implementation is that it would not be easy to compare cards to see which had a higher rank or suit.

See why.

Why: Strings compare alphabetically, which has nothing to do with card order — so every comparison would need a lookup table.

Figure (svg): A flowchart contrasting comparing encoded integers with comparing strings via a lookup

The encoding moves the lookup from every comparison into the data itself.

Readable data and awkward comparison. Integers reverse both, and the chapter chooses comparison because that is what the operations need.

Verify: Ask what the string version would cost.

Why: A dictionary from suit name to rank order, consulted on every comparison — which is exactly the lookup the integer encoding builds into the values themselves. The choice is chapter 13's data structure selection: pick the representation that makes the operations you need straightforward.

11. Trap: assuming the encoding is visible in the code

Trap

The trap

A reader looks for where the program states that Spades is 3, and cannot find it.

Expect the mapping to be written down

Why: It is essential to understanding the program.

The mappings are part of the program design and don't appear explicitly in the code — except indirectly, in the order of the names in suit_names. A reader who misses that has no way to know which number means what.

The fix

Record the encoding where a reader will find it.

The class attribute lists imply it

Why: suit_names[3] is 'Spades', which is the mapping in usable form.

And a docstring can state it outright

Why: Which costs one line and removes the guesswork.

The book's own arrow notation exists precisely because the mapping is not in the code. Anything true of a program that the program does not say is something a reader has to be told separately.

12. Discriminate: what does this integer encode?

Discrimination

Suits run 0 to 3 and ranks 1 to 13.

Sort into buckets

For each value in a Card, what does it mean?

a suit
suit 3; suit 0; suit 2
a rank
rank 13; rank 11; rank 1
s
Suits run from 0 for Clubs to 3 for Spades, so any of these numbers in the suit position names one of the four suits — with higher meaning better.
r
Ranks run from 1 for the Ace to 13 for the King, with the numeric cards mapping to themselves and the face cards continuing the sequence.

13. Complete it: the Card init method

Faded example

Two attributes, both optional.

Fill in the blanks

class Card:
def __init__(self, suit=0, rank=2):
self.suit = suit
self.rank = rank

Why: The init method takes an optional parameter for each attribute and stores each as an attribute of self — chapter 17's pattern unchanged. The defaults make the 2 of Clubs, which is a real card; a default rank of 0 would produce one that does not exist.

14. Explain it yourself: why does the encoding direction matter?

Explain it to yourself

Spades could have been 0.

Discussion prompt

The mapping puts Clubs at 0 and Spades at 3. Why does the direction matter, and what would break if it were reversed?

Hint: What operation is the encoding for?

Answer:

Because the whole point is that higher suits map to higher numbers, so we can compare suits by comparing their codes. The comparison inherits the ordering from the encoding.

Reversed, every comparison would come out backwards — Clubs would beat Spades — and nothing in the code would look wrong, because the code only ever compares numbers.

Which is why the mapping is part of the program design and worth recording: it carries meaning that the code cannot express, and an error in it produces a program that is consistently and invisibly wrong.

15. Class attributes

Section

Section 2

16. Defined inside the class, outside any method

Concept

In order to print Card objects in a way that people can easily read, we need a mapping from the integer codes to the corresponding ranks and suits. A natural way to do that is with lists of strings, assigned to class attributes.

class attribute — An attribute associated with a class object, defined inside a class definition but outside any method.

# inside class Card:
    suit_names = ['Clubs', 'Diamonds', 'Hearts', 'Spades']
    rank_names = [None, 'Ace', '2', '3', '4', '5', '6', '7',
                  '8', '9', '10', 'Jack', 'Queen', 'King']

    def __str__(self):
        return '%s of %s' % (Card.rank_names[self.rank],
                             Card.suit_names[self.suit])
NameWhich kindNote
suit_names, rank_namesclass attributesone copy, shared
suit, rankinstance attributesone per card
Card.rank_names[self.rank]both kinds togetherindex one with the other

This term distinguishes them from variables like suit and rank, which are called instance attributes because they are associated with a particular instance.

Think Python, 2nd edition — Allen B. Downey §18.1-18.3, pp. 172-172

17. Picture it: figure 18.1, one class and one instance

Picture it

Every card has its own suit and rank; there is only one copy of the name lists.

Figure (svg): A class object holding the shared name lists beside an instance holding its own suit and rank

The book's figure 18.1. Every card has its own suit and rank, and there is only one copy of suit_names and rank_names.

Which is the point of a class attribute: the name lists are the same for every card, so storing them fifty-two times would be waste.

18. Worked example: reading the __str__ expression

Worked example

Both kinds of attribute in one subscript.

Card.rank_names[self.rank]

# Card.rank_names  - the class attribute: a list
# self.rank        - the instance attribute: an integer
# the whole thing  - index the list with the integer
PartWhat it givesNote
Card.rank_namesthe listfrom the class
self.rankan indexfrom the instance
the subscripta string'Jack'

Read the class part.

Why: Card is a class object, and Card.rank_names is a list of strings associated with the class.

Read the instance part.

Why: self is a Card object, and self.rank is its rank — an integer.

Put them together.

Why: Use the attribute rank from the object self as an index into the list rank_names from the class Card, and select the appropriate string.

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

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

One string, from a shared list indexed by a per-card number. Both kinds of attribute are accessed using dot notation, which is why the expression reads uniformly.

Verify: Check it on the book's example.

Why: Card(2, 11) has suit 2 and rank 11, which index to 'Hearts' and 'Jack' — so printing it gives Jack of Hearts, exactly as the book shows. Checking a card whose name you can predict is what confirms both lookups rather than just one.

19. Predict: what does printing this card give?

Prediction

Suit 2, rank 11.

card1 = Card(2, 11)
print(card1)
AttributeThe lookupResult
suit 2suit_names[2]'Hearts'
rank 11rank_names[11]'Jack'
the format'%s of %s'rank first

Predict first

What does this print?

  • Jack of Hearts
  • Hearts of Jack
  • 2 of 11
  • 10 of Hearts

Correct: Jack of Hearts — rank_names[11] is 'Jack' and suit_names[2] is 'Hearts'.

Why: The format string puts the rank first, so the arguments are rank_names then suit_names — reversing them would give option B, which is legal and reads wrongly. Option D is what you would get without the None place-keeper, since every rank index would be off by one.

20. Worked example: the None place-keeper

Worked example

A small tweak with a reason and an alternative.

rank_names = [None, 'Ace', '2', '3', ...]
#              ^
#              index 0: no card has rank zero

# so index 2 maps to '2', index 11 to 'Jack'
IndexWhat is thereNote
index 0Nonea place-keeper
index 1'Ace'rank 1
index 2'2'the nice property

Note why it is there.

Why: The first element of rank_names is None because there is no card with rank zero.

Note what it buys.

Why: By including None as a place-keeper, we get a mapping with the nice property that the index 2 maps to the string '2', and so on.

Note the alternative.

Why: To avoid this tweak, we could have used a dictionary instead of a list.

Figure (svg): The rank_names list showing the None place-keeper and the resulting index alignment

One wasted slot buys a mapping where the index and the rank agree.

One wasted slot, in exchange for indices that match the ranks they name. A dictionary would avoid it and would cost the natural indexing.

Verify: Ask what would go wrong without it.

Why: Every index would be off by one, so rank 11 would give 'Queen' — a wrong card name with no error at all. That is the kind of off-by-one the place-keeper exists to prevent, and it is why the tweak is worth a comment.

21. Trap: assigning to a class attribute through an instance

Trap

The trap

A program writes card.suit_names = [...] to change the names for one card.

Assign through the object you have

Why: Reading card.suit_names works, so writing it looks symmetric.

The assignment creates an instance attribute that shadows the class one, so this card differs from every other and nothing indicates why. Reading and writing are not symmetric here.

The fix

Assign to the class, if you mean the shared list.

Card.suit_names = [...]

Why: Which changes it for every card, as intended.

And read through the class too, for clarity

Why: Which is what __str__ does: Card.rank_names, not self.rank_names.

The book's __str__ writes Card.rank_names deliberately. Reading through the class says that the list belongs to the class, which is information the instance form hides.

22. Sort: class attribute or instance attribute?

Sorting

One copy, or one per object?

Sort into buckets

For each, which kind of attribute is it?

a class attribute
suit_names; rank_names; a list shared by every Card
an instance attribute
self.suit; self.rank; a value that differs from card to card
cls
Each is defined inside the class but outside any method, so it is associated with the class object — one copy, shared by every instance.
ins
Each is assigned in __init__ through self, so it belongs to a particular instance and every card has its own.

23. Complete it: index the shared list

Faded example

The list from the class, the index from the instance.

Fill in the blanks

def __str__(self):
return '%s of %s' % (Card.rank_names[self.rank],
Card.suit_names[self.suit])

Why: The expression means: use the attribute rank from the object self as an index into the list rank_names from the class Card, and select the appropriate string. Both kinds of attribute are reached with dot notation — the difference is what comes before the dot.

24. Think it through: why not store the names on each card?

Socratic

Every card could carry its own copy.

Discussion prompt

Each Card could have its own suit_names list, assigned in __init__. What would that cost, and what would it gain?

Hint: How many cards are there?

Answer:

It would gain nothing: the lists are identical for every card, since the encoding is a property of the design rather than of any particular card.

And it would cost fifty-two copies of two lists, plus the risk that one card's copy is edited and the others are not — which would make cards disagree about what suit 3 means.

So a class attribute is the right home for anything shared by every instance. The book's figure makes this exact point: every card has its own suit and rank, and there is only one copy of suit_names and rank_names.

25. Comparing cards

Section

Section 3

26. __lt__, and an ordering that is not obvious

Concept

For built-in types there are relational operators that compare values. For programmer-defined types, we can override the behaviour of the built-in operators by providing a method named __lt__, which stands for less than.

# inside class Card:
    def __lt__(self, other):
        t1 = self.suit, self.rank
        t2 = other.suit, other.rank
        return t1 < t2
PartWhat it doesNote
self, otherthe two operandsas for __add__
the tuplessuit firstthe primary ordering
t1 < t2tuple comparisonelement by element

__lt__ takes two parameters, self and other, and returns True if self is strictly less than other. And the correct ordering for cards is not obvious: which is better, the 3 of Clubs or the 2 of Diamonds?

Think Python, 2nd edition — Allen B. Downey §18.1-18.3, pp. 173-174

27. Picture it: the tuple decides the priority

Picture it

Suit first, because that is the decision that was made.

Figure (svg): Two card tuples compared element by element with the deciding field marked

Chapter 12's rule again: position 0 dominates, and choosing what goes there is the design decision.

The answer might depend on what game you are playing — so the tuple's field order is where an arbitrary choice gets recorded.

28. Worked example: the explicit version and the concise one

Worked example

Five lines, or three.

# the explicit version
    def __lt__(self, other):
        if self.suit < other.suit: return True
        if self.suit > other.suit: return False
        return self.rank < other.rank

# the concise version, using tuple comparison
    def __lt__(self, other):
        t1 = self.suit, self.rank
        t2 = other.suit, other.rank
        return t1 < t2
VersionHow it worksNote
the explicit versionchecks the suits, then the ranksthree returns
the concise versionbuilds tuples and comparesone return
boththe same orderingsuit first

Read the explicit version.

Why: It checks the suits first and only reaches the ranks when the suits are equal — the nesting written out.

Read the concise version.

Why: You can write this more concisely using tuple comparison, which does the same thing in one operation.

Note that the ordering is identical.

Why: Both put suit first, because both were written after that decision was made.

Figure (svg): Two columns comparing an explicit branching comparison with a tuple comparison

Lesson 16a's is_after, in a new setting — and the same argument for the tuple form.

The same comparison twice. The tuple version is shorter and less error-prone, because the element-by-element rule cannot be got wrong the way three explicit branches can.

Verify: Check the equal case in both.

Why: Two identical cards give False from both versions — the explicit one falls through to a rank comparison that is also False, and the tuple one runs out of elements. Testing equality matters because it is the case the explicit version handles by accident rather than deliberately.

29. Predict: which card is less?

Prediction

Suit is more important.

# 3 of Clubs:     suit 0, rank 3
# 2 of Diamonds:  suit 1, rank 2
print(Card(0, 3) < Card(1, 2))
FieldThe valuesEffect
suits0 and 1differ
the comparisonstops there0 < 1
the ranksnever examined3 vs 2 is irrelevant

Predict first

What does this print?

  • True
  • False
  • An error
  • It depends on the game

Correct: True — the suits differ, so the comparison is decided there, and Clubs is lower than Diamonds.

Why: The tuple comparison stops at the first difference, so the higher rank of the 3 of Clubs never enters into it. That is the arbitrary choice made concrete: suit is more important, so all the Diamonds outrank all the Clubs whatever their ranks.

30. Worked example: where the arbitrary choice lives

Worked example

One line records a decision nothing else implies.

        t1 = self.suit, self.rank        # suit is more important

# reversed:
        t1 = self.rank, self.suit        # rank is more important
Field orderWhat it meansNote
suit firstall Spades outrank all Diamondsthe book's choice
rank firstall Kings outrank all Queensa different game
the codeidentical apart from the order

Note that the problem does not decide.

Why: The correct ordering for cards is not obvious — one card has a higher rank and the other a higher suit.

Note that the game might.

Why: The answer might depend on what game you are playing, which means the class is committing to one game's rules.

Note where the commitment is recorded.

Why: In the order of two names on one line, which is the only place the decision appears.

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

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

An arbitrary choice, recorded as a field order. The book makes it explicitly — suit is more important — which is worth doing, because the code alone would not say so.

Verify: Ask how a reader would discover the ordering.

Why: By reading __lt__ and noticing which field comes first in the tuple — there is nowhere else it appears. A comment or a docstring saying suit outranks rank costs one line and saves that inference, which matters because the alternative is equally plausible.

31. Trap: building the tuples in different orders

Trap

The trap

A comparison builds t1 as (suit, rank) and t2 as (rank, suit).

Write each tuple from its own object

Why: Two similar lines, typed separately.

The comparison then matches a suit against a rank, which is legal since both are integers — so it produces confident nonsense with no error at all.

The fix

Build both tuples the same way.

t1 = self.suit, self.rank and t2 = other.suit, other.rank

Why: The same field order in both.

Or write it as one expression

Why: return (self.suit, self.rank) < (other.suit, other.rank), which makes the symmetry visible.

Both fields are integers, so nothing about the types can catch the mistake. Writing the comparison as a single line is the cheapest defence, because the asymmetry becomes visible.

32. Complete it: order by suit, then rank

Faded example

The field order records the decision.

Fill in the blanks

def __lt__(self, other):
t1 = self.suit, self.rank
t2 = other.suit, other.rank
return t1 < t2

Why: Putting the suit at position 0 makes it the primary ordering, since tuples compare element by element from the left. Both tuples must use the same field order — building one as (suit, rank) and the other as (rank, suit) would compare a suit against a rank, which is legal and meaningless.

33. Two truths and a lie: comparing cards

Two truths and a lie

Two are true. Keep the lie.

Eliminate the wrong options

Rule out the two true statements.

  • A. The correct ordering for cards is not obvious, and may depend on the game
  • B. __lt__ takes self and other and returns True if self is strictly less than other
  • C. Python can work out a sensible ordering for a class without being told

Survives elimination: C

Why: C is the same misunderstanding as chapter 15's == surprise. Python has no way to know whether suit or rank should dominate — the answer depends on the game, which is outside the program entirely. Defining __lt__ is how the decision gets recorded, and the field order is where it lives.

34. Explain it: which card wins?

Explain it

A question with no answer until someone decides.

Discussion prompt

A classmate asks whether the 3 of Clubs beats the 2 of Diamonds. Explain why you cannot answer without more information, and where the answer lives once it is decided.

Hint: One has a higher rank and the other a higher suit.

Answer:

The cards do not settle it: one has a higher rank and the other a higher suit, so the answer depends on which matters more — which depends on the game.

Once decided, the answer lives in the order of the fields in __lt__'s tuple. Suit first means all the Diamonds beat all the Clubs; rank first would mean the opposite.

Which is why the book calls it an arbitrary choice and says so out loud. Nothing in the data implies it, so a reader can only learn it from the code or a comment — and a comment is cheaper to read.

35. What defining __lt__ gives you

Section

Section 4

36. One method, several operators

Concept

Defining __lt__ makes the less-than operator work on Cards — and it also makes anything built on ordering work, because the built-in functions and methods that sort or compare use the same operator.

card1 < card2       # __lt__ directly

sorted(cards)       # uses __lt__
cards.sort()        # uses __lt__
max(cards)          # uses __lt__
min(cards)          # uses __lt__
ExpressionWhat it usesNote
the operatorinvokes __lt__directly
sortingcompares pairswith the same operator
max and minlikewiseone method serves all

This is chapter 17's polymorphism from the other side: those functions work on Cards because Cards support the operation they use, and nobody had to teach sorted about playing cards.

Think Python, 2nd edition — Allen B. Downey §18.1-18.3, pp. 173-176

37. Picture it: one method, many callers

Picture it

Everything that orders things uses the same comparison.

Figure (svg): A pipeline showing several built-in operations all reaching the same comparison method

One method, and every ordering operation in the language can use your class.

Which is why the book's exercise — write a Deck method named sort — is a one-liner: sort uses the __lt__ method we defined to determine the order.

38. Worked example: sorting a list of cards

Worked example

The sort method needs nothing but the comparison.

cards = [Card(1, 12), Card(0, 3), Card(3, 1)]
cards.sort()
for c in cards:
    print(c)

# 3 of Clubs
# Queen of Diamonds
# Ace of Spades
CardIts suitPosition
suit 0Clubsfirst
suit 1Diamondssecond
suit 3Spadeslast

Call sort.

Why: It compares pairs of elements, and for Cards that comparison is the __lt__ method you defined.

Note the resulting order.

Why: By suit first, since that is what the tuple puts at position 0 — so the Ace of Spades comes last despite its low rank.

Note what sort needed to know.

Why: Nothing about cards: it needed a way to compare two elements, and the class supplied one.

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

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

The cards in suit order, from a method that has never heard of playing cards. The exercise's Deck.sort is one line for exactly this reason.

Verify: Check a pair with the same suit.

Why: Two Clubs sort by rank, because the tuple comparison moves on when the first elements match — so the ordering is suit-major and rank-minor, which is what the field order specified. Testing a tie is what exercises the second field at all.

39. Predict: does sorting work?

Prediction

The class defines __lt__ and nothing else.

cards = [Card(1, 12), Card(0, 3)]
cards.sort()
print(cards[0])
StepWhat happensResult
sortcompares pairsusing __lt__
suit 0 against suit 1Clubs is lowersorts first
cards[0]the 3 of Clubs

Predict first

What does this print?

  • 3 of Clubs
  • Queen of Diamonds
  • A TypeError, since Cards cannot be sorted
  • The card objects' addresses

Correct: 3 of Clubs — sort uses __lt__, and suit 0 sorts before suit 1.

Why: sort needs a way to compare two elements and nothing else, so defining __lt__ is enough to make it work — chapter 17's polymorphism from the other side. And printing shows the card's name because the class also defines __str__, without which option D would be right.

40. Worked example: what __lt__ does not give you

Worked example

One method, and not every operator.

card1 < card2      # works: __lt__
card1 > card2      # works: Python reverses the operands
card1 == card2     # does NOT: needs __eq__
card1 <= card2     # may not: needs __le__
OperatorWhich methodNote
less than__lt__defined
greater thanthe reversed comparisonderived
equality__eq__a separate method

Note what comes free.

Why: Greater-than can be answered by reversing the operands, so defining less-than covers it.

Note what does not.

Why: Equality is a different question, and without __eq__ it falls back on identity — chapter 15's disappointment, unchanged.

Draw the conclusion.

Why: Two Cards with the same suit and rank compare unequal by default, however sensible it would be for them to match.

Figure (svg): Two columns separating what defining __lt__ provides from what it does not

Ordering and equality are different questions, with different methods.

Ordering, and not equality. The two are separate methods because they are separate questions, and a class can reasonably answer one and not the other.

Verify: Test two identical cards.

Why: Card(1, 12) == Card(1, 12) reports False without an __eq__ method, because for instances the default behaviour of == is the same as is. Defining __eq__ to compare the tuples would fix it, and it is a separate decision from the ordering.

41. Trap: expecting equality from an ordering method

Trap

The trap

A program defines __lt__ and then tests whether two cards are the same with ==.

Assume comparison is one feature

Why: Ordering and equality both feel like comparing.

== falls back on identity without __eq__, so two cards with the same suit and rank report False. The test never matches and nothing raises.

The fix

Define __eq__ as well, if equality is meant to be by value.

return (self.suit, self.rank) == (other.suit, other.rank)

Why: The same fields, the same shape.

Or compare the attributes explicitly at the call site

Why: Which says what you mean without changing the class.

Chapter 15 flagged this and said at least, not yet. This is the yet: the mechanism is the same special-method mechanism, and equality simply needs its own.

42. Discriminate: does __lt__ make this work?

Discrimination

Ordering, or something else?

Sort into buckets

For each operation on Cards, does defining __lt__ suffice?

yes — it uses ordering
cards.sort(); max(cards); sorted(cards)
no — it needs another method
card1 == card2; print(card1); card1 + card2
yes
Each needs only a way to compare two elements, which __lt__ provides — so it works on any class defining it, without knowing anything else about the type.
no
Each needs a different special method: __eq__ for equality, __str__ for printing, __add__ for addition. Ordering does not imply any of them.

43. Complete it: sort a deck's cards

Faded example

The list method does the work, using your comparison.

Fill in the blanks

def sort(self):
self.cards.sort()

Why: The list method sort uses the __lt__ method we defined to determine the order, so the Deck method is a single line delegating to it. This is the exercise the book sets, and it is short precisely because the comparison was defined once on the Card class.

44. Where defining one comparison unlocks everything

Real world

Sorting, ranking, choosing a maximum.

Discussion prompt

Think of a collection you have wanted ordered several different ways. What had to be decided once for each ordering?

Hint: By date, by size, by name.

Answer:

Which field takes priority, and which breaks ties. Once that is settled, sorting, finding the largest and picking the top ten all follow from the same rule.

Which is why one comparison method serves so many operations: they all reduce to is this one before that one?, and none of them needs to know anything else about the items.

And when you want a second ordering, the decision has to be made again — which is why sorted also takes a key function, so the ordering can be supplied per call rather than fixed in the class.

45. Recording decisions the code cannot state

Section

Section 5

46. Two design decisions, neither visible

Concept

This lesson has made two choices that the code does not explain: the encoding from numbers to suits, and the priority of suit over rank.

The book writes the encoding out with an arrow notation to make it clear that these mappings are not part of the Python program — they are part of the program design, and they don't appear explicitly in the code.

Think Python, 2nd edition — Allen B. Downey §18.1-18.3, pp. 172-173

47. Picture it: where each decision is hiding

Picture it

Both are recorded, and neither is stated.

Figure (svg): Two columns pairing each design decision with the single place it appears in the code

Four decisions, each visible only as a detail that could plausibly have been otherwise.

Every one of these could be reversed without the code looking wrong, which is exactly why they are worth writing down.

48. Worked example: what a comment would add

Worked example

One line each, and they are cheap.

class Card:
    """Represents a standard playing card.

    suits:  0 Clubs, 1 Diamonds, 2 Hearts, 3 Spades
    ranks:  1 Ace through 13 King
    order:  suit is more important than rank
    """
DecisionNowNote
the suit encodingstatednot inferred from a list
the rank encodingstatedincluding the Ace at 1
the orderingstatednot inferred from a tuple

State the encodings.

Why: So that a reader does not have to count positions in suit_names to work out what 3 means.

State the ordering.

Why: So that the arbitrary choice is visible as a choice rather than as an incidental field order.

Note the cost.

Why: Three lines in a docstring, written once, against every future reader inferring them from details.

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

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

A docstring that says what the code cannot. The information was always there and required inference; now it requires reading.

Verify: Check whether the docstring could go stale.

Why: It could — reordering suit_names without updating it would make it a lie, which is worse than silence. That is the standing cost of documentation, and it is why the docstring should state decisions rather than restate code: suit is more important is a fact about intent, which changes only when the intent does.

49. Predict: is the Ace high or low?

Prediction

The encoding decides it.

rank_names = [None, 'Ace', '2', '3', ...]
#                     ^ index 1
CardIts rankNote
the Acerank 1the lowest
the Kingrank 13the highest
comparisonby these numbersso the Ace is low

Predict first

In this encoding, does the Ace beat the King?

  • No — the Ace is rank 1 and the King is 13
  • Yes — Aces are always high
  • It depends on the suit
  • They compare equal

Correct: No — the Ace is rank 1, which is the lowest, so the King beats it.

Why: The book notes that depending on the game, an Ace may be higher than King or lower than 2 — and this encoding chooses low. Nothing marks that as a decision; the Ace is simply the first entry after the place-keeper, which is exactly the kind of silent choice worth documenting.

50. Worked example: the decisions this chapter still leaves open

Worked example

Some questions the Card class has not answered.

# is an Ace high or low?
#   the book: 'depending on the game that you are playing,
#    an Ace may be higher than King or lower than 2'
#   the encoding: rank 1, so lower than 2

# are two cards with the same suit and rank equal?
#   no __eq__, so no
QuestionWhat the class saysNote
the Aceencoded as 1so it is low
the alternativehigh, above the Kingwould need 14
equalityundefinedfalls back on identity

Note the Ace decision.

Why: Depending on the game, an Ace may be higher than King or lower than 2 — and this encoding puts it at 1, which makes it low.

Note that it was made silently.

Why: Nothing in the class marks it as a choice; the Ace is simply the first entry after the place-keeper.

Note the open question.

Why: Equality is undefined, so two identical cards are unequal — which no game would agree with.

Figure (svg): Four decisions the Card class makes and where each is recorded

The last one is recorded by something not being there, which is the hardest kind to notice.

Two more decisions, one made silently and one left open. Both are reasonable, and both would surprise someone who assumed the class modelled cards in general.

Verify: Ask what an Ace-high game would need.

Why: Either a different encoding — the Ace at 14 — or a comparison that special-cases rank 1, which would be the harder route. Noticing that the encoding decides this, rather than the comparison, is the useful observation: the representation constrains what the operations can easily express.

51. Trap: assuming a class models the general case

Trap

The trap

A program uses the Card class for a game where Aces are high and rank matters more than suit.

Reuse a class that represents the same thing

Why: It is a standard playing card, and the game uses standard playing cards.

The class encodes one game's rules: the Ace is low and suit dominates. Both are silently wrong for the new game, and nothing raises — the cards simply rank incorrectly.

The fix

Check which decisions the class has made.

Read the encoding and the comparison

Why: Which is where the game-specific choices live.

And prefer a class that states them

Why: So the check is reading rather than inferring.

The book is careful about this: the answer might depend on what game you are playing, and it makes an arbitrary choice to keep things simple. A class that models a card is not the same as a class that models every game's cards.

52. Sort: stated in the code, or only implied?

Sorting

Some facts appear and some are inferred.

Sort into buckets

For each fact about Cards, is it stated in the code or only implied?

stated
a Card has a suit and a rank; printing gives 'rank of suit'; the default card is the 2 of Clubs
only implied
Spades is the highest suit; suit matters more than rank; the Ace is low
st
Each appears directly: the attributes in __init__, the format string in __str__, and the parameter defaults. A reader sees them rather than deducing them.
im
Each has to be inferred from a detail that could plausibly have been otherwise — a list's order, a tuple's field order, or an encoding choice. None is written down as a decision.

53. Complete it: document the ordering

Faded example

One line records an arbitrary choice.

Fill in the blanks

class Card:
"""Represents a standard playing card.

order: suit is more important than rank
"""

Why: The book makes the arbitrary choice that suit is more important, so all the Spades outrank all the Diamonds. That decision appears in the code only as the field order inside __lt__'s tuple, so stating it in the docstring saves every reader the inference.

54. Explain it: how do I know what suit 3 means?

Explain it

The number is in the object and the meaning is not.

Discussion prompt

A classmate has a Card with suit 3 and wants to know which suit that is. Tell them where to look, and why it is not obvious.

Hint: The mapping is not written down as a mapping.

Answer:

They have to look at suit_names and count: it is a list, and index 3 is 'Spades'. The mapping exists only as the order of those four strings.

Which is why the book writes the tables out with arrows and says explicitly that these mappings are not part of the Python program — they are part of the program design.

So the practical advice is to state the encoding in the class's docstring. It costs one line, and it turns a fact that has to be inferred from a list's order into one that can be read.

55. Compare: class attributes and instance attributes

Comparison

Fill the blanks. Both use dot notation.

Comparison matrix

QuestionClass attributeInstance attribute
Where is it defined?inside the class, outside any methodassigned through self, usually in __init__
How many copies?one, shared by every instanceone per instance
An example heresuit_namesself.suit
How is it reached?Card.suit_namesself.suit

Both kinds are accessed using dot notation — what differs is whether the class or the instance comes before the dot.

56. The procedure: designing a class around an encoding

Pattern

Six steps, and the last is the one that is easiest to skip.

  1. Decide what the attributes are — here, obviously suit and rank.
  2. Decide what type they should be, by asking which operations you need: comparison favoured integers over strings.
  3. Build the mapping so that the ordering you want falls out of the numbers.
  4. Put the name lists in class attributes, since they are the same for every instance.
  5. Define __lt__ with a tuple whose field order records which attribute dominates.
  6. Write the encoding and the ordering into the docstring, because the code cannot state either.

Step 2 is chapter 13's data structure selection in miniature: the encoding was chosen because it makes the operation you need — comparison — into something the language already does.

Python documentation — Classes Classes

57. Check yourself 1 of 3: class attributes

Check

Two lists and two numbers.

Check your understanding

Why are suit_names and rank_names class attributes rather than instance attributes?

  • A. Because they are the same for every card, so one shared copy is enough (correct)
  • B. Because lists cannot be instance attributes
  • C. Because __str__ cannot reach instance attributes
  • D. Because they are constants

Answer: A

Why: Every card has its own suit and rank, but there is only one copy of suit_names and rank_names. A class attribute is associated with the class object, which is what makes it the right home for anything shared by every instance.

Why B tempts people
Instance attributes can hold any type, lists included — the Deck class stores one in the next lesson.
Why C tempts people
__str__ uses self.rank, which is an instance attribute, in the same expression.
Why D tempts people
Nothing prevents a class attribute from changing; being shared is the property that matters here.

58. Check yourself 2 of 3: the place-keeper

Check

The first element of rank_names.

Check your understanding

Why is the first element of rank_names None?

  • A. So that index 2 maps to '2', and so on — there is no card with rank zero (correct)
  • B. Because lists must begin with None
  • C. To mark the end of the list
  • D. Because the Ace has no name

Answer: A

Why: There is no card with rank zero, and by including None as a place-keeper we get a mapping with the nice property that the index matches the rank it names. The book notes that a dictionary would avoid the tweak, at the cost of the natural indexing.

Why B tempts people
Nothing requires it; suit_names begins with 'Clubs' because suit 0 is a real suit.
Why C tempts people
Lists need no terminator; len gives their length.
Why D tempts people
The Ace is at index 1 and is named. The None is at index 0, which no card uses.

59. Check yourself 3 of 3: the ordering

Check

Suit is at position 0 of the tuple.

    def __lt__(self, other):
        t1 = self.suit, self.rank
        t2 = other.suit, other.rank
        return t1 < t2
FieldIts roleNote
position 0the suitthe primary ordering
position 1the rankthe tie-break
the ruleelement by elementchapter 12

Check your understanding

What does this ordering mean for the cards?

  • A. All the Spades outrank all the Diamonds, whatever their ranks (correct)
  • B. All the Kings outrank all the Queens, whatever their suits
  • C. Cards are ordered by the sum of suit and rank
  • D. The ordering depends on which card is compared first

Answer: A

Why: The suit is at position 0, so tuple comparison decides on it whenever the suits differ and never reaches the ranks. That is the book's arbitrary choice — suit is more important — and swapping the two fields would give option B instead.

Why B tempts people
This would be the result of putting rank first, which is equally defensible and is not what the code does.
Why C tempts people
Nothing is summed. Tuple comparison examines fields in order.
Why D tempts people
The comparison is symmetric: a < b and b < a always disagree unless the cards are equal.

60. Where this shows up outside this course

Real world

Encoding categories as numbers so they sort correctly.

Discussion prompt

Think of somewhere categories are stored as codes rather than names. What does the coding buy, and what does it cost a reader?

Hint: Sizes, grades, priority levels.

Answer:

Sizes stored as 1 to 5, priority levels as numbers, grades as points — in each case the codes sort correctly by themselves, where the names would sort alphabetically and wrongly.

What it costs is legibility: a 3 means nothing without the key, and anyone reading the raw data has to be told. The mapping lives outside the data.

Which is exactly the book's point about the arrow notation. The encoding is part of the design and does not appear in the code, so it has to be recorded somewhere a reader will find it — or every reader reconstructs it from a list's order.

61. Confidence wager: commit before you check

Commit first

Answer, then rate your confidence.

Predict first

Why is the first element of rank_names None?

  • So that the index matches the rank — index 2 maps to '2' — since no card has rank zero
  • Because a list must have a placeholder at index 0
  • To mark where the face cards begin
  • Because the Ace is worth nothing

Correct: So that the index matches the rank — index 2 maps to '2' — since no card has rank zero.

Why: The ranks run from 1 for the Ace to 13 for the King, and lists are indexed from 0 — so without a place-keeper every lookup would be off by one and rank 11 would give 'Queen'. That would be a wrong card name with no error at all, which is exactly the kind of silent off-by-one worth engineering away. The book puts it as a nice property: by including None, the index and the rank agree, so rank_names[self.rank] needs no arithmetic. It also names the alternative — to avoid this tweak, we could have used a dictionary instead of a list — which would map ranks to names directly and give up the natural indexing. That is a data structure decision in miniature, and worth noticing as one: neither option is wrong, and each makes a different thing easy.

62. Explain it to someone else

Explain it

Numbers where names would be clearer.

Discussion prompt

A classmate asks why the cards store integers rather than readable strings like 'Spades'. Give them the reason and the cost.

Hint: What operation does the class need?

Answer:

Comparison. With strings, working out which card is higher would need a lookup table on every comparison, because strings order alphabetically and that has nothing to do with card order.

With integers the ordering is built into the values: higher suits map to higher numbers, so comparing suits is comparing their codes, and the tuple comparison does the whole job in one line.

The cost is legibility — Card(1, 12) tells you nothing without the key — which is why the class carries suit_names and rank_names to translate back for printing, and why the encoding is worth stating in the docstring.

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 encoding, and why integers were chosen over strings
  • Class attributes against instance attributes
  • __lt__, and how the tuple records the ordering decision
  • Which design decisions the code cannot state

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

Why: The encoding is chapter 13's data structure selection in miniature: the representation was chosen to make comparison easy, and the readability cost is paid back by the name lists. The two kinds of attribute differ in how many copies exist, and the expression Card.rank_names[self.rank] uses both in one subscript. The comparison is chapter 12's tuple rule with an arbitrary choice recorded in the field order. And the last one is the reading skill that matters most: noticing which facts about a program are true without being stated anywhere.

64. Synthesis: draw the map of this lesson

Connect it up

One page, from memory.

Draw it

Draw the book's figure 18.1: the class object holding the two name lists, and one instance holding its own suit and rank. Beside it, write the expression Card.rank_names[self.rank] and label which part comes from which. Underneath, write __lt__ using tuple comparison and circle the field that records the arbitrary choice. Finally list two facts about the Card class that are true and stated nowhere in the code.

65. What you can do now

Recap

Three pages, and a class ready for the deck to be built from.

If you remember one thingIt is this
From the encodingChoose the representation that makes the operation you need easy.
From class attributesOne copy for the class; one copy per instance for the others.
From the place-keeperOne wasted slot, and the index matches the rank.
From __lt__The tuple's field order is where the arbitrary choice lives.
From the design decisionsThe mappings are part of the design and do not appear in the code.

The next lesson builds a Deck from these Cards: a nested loop generating all fifty-two, a __str__ that joins them with newlines, and the add, remove, shuffle and sort methods — including the veneer, a thin method that expresses a list operation in terms appropriate for decks.

Think Python, 2nd edition — Allen B. Downey §18.1-18.3, pp. 171-173 — 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.1-18.3, pp. 171-173
  2. Python documentation — Classes
  3. Python documentation — Built-in Types

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

Book on Wyzant · Text (657) 465-8108