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
Title
Python · Chapter 18 — Inheritance
§18.1-18.3, pp. 171-173
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
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.
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
Think Python, 2nd edition — Allen B. Downey §18.1-18.3, pp. 171-172
Section
Section 1
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| Group | The codes | Note |
|---|---|---|
| the suits | 0 to 3 | in ascending order for bridge |
| the numeric ranks | each maps to itself | 2 is 2 |
| the face cards | 11, 12, 13 | continuing 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
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
The mapping's direction is the design decision. Reversing it would make Clubs beat Spades, and nothing in the code would look wrong.
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)| Parameter | What the default means | Note |
|---|---|---|
| suit=0 | Clubs | the default |
| rank=2 | the 2 | the default |
| Card(1, 12) | Diamonds, Queen | the 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
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.
Prediction
Suit first, then rank.
card = Card(1, 12)
# suit 1, rank 12| Code | What it means | Note |
|---|---|---|
| suit 1 | Diamonds | from the mapping |
| rank 12 | Queen | likewise |
| the card | the queen of diamonds |
Predict first
Which card does this create?
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.
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| Aspect | What happens | Note |
|---|---|---|
| strings compare | alphabetically | not by card value |
| Spades and Hearts | H before S | so Hearts wins, wrongly |
| the fix | a lookup table per comparison | or 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
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.
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.
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.
Discrimination
Suits run 0 to 3 and ranks 1 to 13.
Sort into buckets
For each value in a Card, what does it mean?
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.
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.
Section
Section 2
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])| Name | Which kind | Note |
|---|---|---|
| suit_names, rank_names | class attributes | one copy, shared |
| suit, rank | instance attributes | one per card |
| Card.rank_names[self.rank] | both kinds together | index 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
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
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.
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| Part | What it gives | Note |
|---|---|---|
| Card.rank_names | the list | from the class |
| self.rank | an index | from the instance |
| the subscript | a 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
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.
Prediction
Suit 2, rank 11.
card1 = Card(2, 11)
print(card1)| Attribute | The lookup | Result |
|---|---|---|
| suit 2 | suit_names[2] | 'Hearts' |
| rank 11 | rank_names[11] | 'Jack' |
| the format | '%s of %s' | rank first |
Predict first
What does this print?
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.
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'| Index | What is there | Note |
|---|---|---|
| index 0 | None | a 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, 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.
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.
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.
Sorting
One copy, or one per object?
Sort into buckets
For each, which kind of attribute is it?
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.
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.
Section
Section 3
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| Part | What it does | Note |
|---|---|---|
| self, other | the two operands | as for __add__ |
| the tuples | suit first | the primary ordering |
| t1 < t2 | tuple comparison | element 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
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
The answer might depend on what game you are playing — so the tuple's field order is where an arbitrary choice gets recorded.
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| Version | How it works | Note |
|---|---|---|
| the explicit version | checks the suits, then the ranks | three returns |
| the concise version | builds tuples and compares | one return |
| both | the same ordering | suit 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
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.
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))| Field | The values | Effect |
|---|---|---|
| suits | 0 and 1 | differ |
| the comparison | stops there | 0 < 1 |
| the ranks | never examined | 3 vs 2 is irrelevant |
Predict first
What does this print?
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.
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 order | What it means | Note |
|---|---|---|
| suit first | all Spades outrank all Diamonds | the book's choice |
| rank first | all Kings outrank all Queens | a different game |
| the code | identical 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
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.
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.
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.
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.
Two truths and a lie
Two are true. Keep the lie.
Eliminate the wrong options
Rule out the two true statements.
Survives elimination: C
Why: C 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.
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.
Section
Section 4
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__| Expression | What it uses | Note |
|---|---|---|
| the operator | invokes __lt__ | directly |
| sorting | compares pairs | with the same operator |
| max and min | likewise | one 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
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
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.
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| Card | Its suit | Position |
|---|---|---|
| suit 0 | Clubs | first |
| suit 1 | Diamonds | second |
| suit 3 | Spades | last |
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 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.
Prediction
The class defines __lt__ and nothing else.
cards = [Card(1, 12), Card(0, 3)]
cards.sort()
print(cards[0])| Step | What happens | Result |
|---|---|---|
| sort | compares pairs | using __lt__ |
| suit 0 against suit 1 | Clubs is lower | sorts first |
| cards[0] | the 3 of Clubs |
Predict first
What does this print?
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.
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__| Operator | Which method | Note |
|---|---|---|
| less than | __lt__ | defined |
| greater than | the reversed comparison | derived |
| 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 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.
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.
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.
Discrimination
Ordering, or something else?
Sort into buckets
For each operation on Cards, does defining __lt__ suffice?
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.
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.
Section
Section 5
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
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
Every one of these could be reversed without the code looking wrong, which is exactly why they are worth writing down.
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
"""| Decision | Now | Note |
|---|---|---|
| the suit encoding | stated | not inferred from a list |
| the rank encoding | stated | including the Ace at 1 |
| the ordering | stated | not 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
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.
Prediction
The encoding decides it.
rank_names = [None, 'Ace', '2', '3', ...]
# ^ index 1| Card | Its rank | Note |
|---|---|---|
| the Ace | rank 1 | the lowest |
| the King | rank 13 | the highest |
| comparison | by these numbers | so the Ace is low |
Predict first
In this encoding, does the Ace beat the King?
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.
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| Question | What the class says | Note |
|---|---|---|
| the Ace | encoded as 1 | so it is low |
| the alternative | high, above the King | would need 14 |
| equality | undefined | falls 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
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.
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.
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.
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?
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.
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.
Comparison
Fill the blanks. Both use dot notation.
Comparison matrix
| Question | Class attribute | Instance attribute |
|---|---|---|
| Where is it defined? | inside the class, outside any method | assigned through self, usually in __init__ |
| How many copies? | one, shared by every instance | one per instance |
| An example here | suit_names | self.suit |
| How is it reached? | Card.suit_names | self.suit |
Both kinds are accessed using dot notation — what differs is whether the class or the instance comes before the dot.
Pattern
Six steps, and the last is the one that is easiest to skip.
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
Check
Two lists and two numbers.
Check your understanding
Why are suit_names and rank_names class attributes rather than instance attributes?
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.
Check
The first element of rank_names.
Check your understanding
Why is the first element of rank_names None?
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.
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| Field | Its role | Note |
|---|---|---|
| position 0 | the suit | the primary ordering |
| position 1 | the rank | the tie-break |
| the rule | element by element | chapter 12 |
Check your understanding
What does this ordering mean for the cards?
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.
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.
Commit first
Answer, then rate your confidence.
Predict first
Why is the first element of rank_names None?
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.
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.
Exit ticket
One honest answer. It decides what the next lesson opens with.
Predict first
Which of these is still least solid for you?
Correct: Whichever you picked is the right answer — this one is for you, not for a mark.
Why: The 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.
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.
Recap
Three pages, and a class ready for the deck to be built from.
| If you remember one thing | It is this |
|---|---|
| From the encoding | Choose the representation that makes the operation you need easy. |
| From class attributes | One copy for the class; one copy per instance for the others. |
| From the place-keeper | One wasted slot, and the index matches the rank. |
| From __lt__ | The tuple's field order is where the arbitrary choice lives. |
| From the design decisions | The 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
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.