This lesson makes operators work on programmer-defined types with special methods, handles operands of different types with dispatch and the right-side add, and closes with polymorphism — functions that work on types they were never written for.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Chapter 17 — Classes and methods
§17.7-17.11, pp. 165-169
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §17.7-17.11, pp. 165-169 — the pages these objectives are drawn from
Warm-up
The same operator, on three types.
Discussion prompt
The plus sign adds integers, concatenates strings, and joins lists. Given what the last lesson said about print invoking __str__, what do you now suspect is happening?
Hint: print did not decide how to display a Time.
Answer:
Each type must define what plus means for it, and the operator asks the object rather than deciding for itself — exactly as print asked the Time for its string.
Which suggests that if you defined the right method on your own class, the plus sign would work on your objects too.
That is exactly right, and it is this lesson. For every operator in Python there is a corresponding special method.
Concept
By defining other special methods, you can specify the behaviour of operators on programmer-defined types. For example, if you define a method named __add__ for the Time class, you can use the + operator on Time objects.
operator overloading — Changing the behaviour of an operator like + so it works with a programmer-defined type.
# inside class Time:
def __add__(self, other):
seconds = self.time_to_int() + other.time_to_int()
return int_to_time(seconds)
>>> print(start + duration)
11:20:00| What you write | What Python does | Note |
|---|---|---|
| start + duration | invokes __add__ | with duration as other |
| print(...) | invokes __str__ | on the result |
| one line | two special methods | behind the scenes |
When you apply the + operator to Time objects, Python invokes __add__; when you print the result, Python invokes __str__. So there is a lot happening behind the scenes.
Figure (svg): A pipeline showing one printed expression invoking two special methods
Think Python, 2nd edition — Allen B. Downey §17.7-17.11, pp. 165-166
Section
Section 1
Concept
Changing the behaviour of an operator so that it works with programmer-defined types is called operator overloading. For every operator in Python there is a corresponding special method, like __add__.
# inside class Time:
def __add__(self, other):
seconds = self.time_to_int() + other.time_to_int()
return int_to_time(seconds)
>>> start = Time(9, 45)
>>> duration = Time(1, 35)
>>> print(start + duration)
11:20:00| Part | What it receives | Note |
|---|---|---|
| self | the left operand | start |
| other | the right operand | duration |
| the return value | a new Time | which is what + produces |
The method takes self and other, exactly like is_after — the left operand becomes the subject and the right becomes the argument. And the body is chapter 16's add_time unchanged, which is the point: the operator is new syntax for an operation that already existed.
Think Python, 2nd edition — Allen B. Downey §17.7-17.11, pp. 165-166
Picture it
The operator is a method call written differently.
Figure (svg): A call diagram showing an addition expression mapped onto a method call
Which is why the left operand matters so much, and why the next idea's problem arises when it is not a Time.
Worked example
The book's own observation: a lot happening behind the scenes.
>>> print(start + duration)
11:20:00
# 1. start + duration invokes start.__add__(duration)
# 2. __add__ calls time_to_int on both
# 3. it calls int_to_time to build a Time
# 4. print invokes __str__ on that Time
# 5. print displays the returned string| Stage | What is invoked | Note |
|---|---|---|
| the operator | invokes __add__ | one special method |
| inside | two conversions | ordinary methods |
| invokes __str__ | another special method |
Trace the operator.
Why: When you apply the + operator to Time objects, Python invokes __add__ with the left operand as the subject.
Trace the body.
Why: It converts both to integers, adds them, and builds a new Time — chapter 16's designed-development approach, reused unchanged.
Trace the printing.
Why: When you print the result, Python invokes __str__ — so a single expression involves two methods that are never named.
Figure (svg): The state of the program after each line of Worked example what one line actually does, drawn as a ladder with one rung per traced line
Five steps from one short line, two of them methods invoked by syntax. That is the mechanism working, and it is worth tracing once so it stops feeling like magic.
Verify: Check that the arithmetic is the chapter 16 version.
Why: The body is time_to_int on both, add, and int_to_time — identical to add_time from lesson 16b. So no new logic was written: the operator is new syntax for an operation that already existed, which is what makes overloading cheap to add.
Prediction
Two Time objects.
start = Time(9, 45)
duration = Time(1, 35)
print(start + duration)| Expression | What is invoked | Note |
|---|---|---|
| start + duration | invokes __add__ | on start |
| duration | becomes other | the right operand |
| invokes __str__ | on the result |
Predict first
What does this print?
Correct: 11:20:00 — the plus sign invokes __add__ and print invokes __str__.
Why: Both special methods are called by syntax that never names them, which is why the book says there is a lot happening behind the scenes. Option B is what you would get without a __str__ method, and option C without an __add__ — each missing method removes one of the two steps.
Worked example
For every operator there is a corresponding special method.
# __add__ +
# __sub__ -
# __mul__ *
# __lt__ <
# __eq__ ==
# __len__ len(x)
# __getitem__ x[i]| Category | How many methods | Note |
|---|---|---|
| arithmetic operators | one method each | add, sub, mul |
| comparisons | likewise | lt, eq |
| built-in functions | also | len, indexing |
Note the generality.
Why: For every operator in Python there is a corresponding special method — the plus sign is one instance of a general mechanism.
Note that built-ins are included.
Why: len and indexing work the same way, which is why they have always worked across the built-in types.
Note what this makes possible.
Why: A class can be given the same syntax as a built-in type, so code written for one can work on the other.
Figure (svg): Operators and built-in functions paired with the special methods they invoke
A method per operator, which is what makes the mechanism general. The next idea's __radd__ is another member of the same family.
Verify: Connect it to chapter 15's disappointment.
Why: Two Points with identical coordinates compared unequal, because Python doesn't know what should be considered equivalent for a programmer-defined type. Defining __eq__ is how you tell it — which is what the book's at least, not yet was pointing at, and it is the same mechanism as __add__.
Trap
An __add__ method adds the other Time's seconds into self and returns self.
Do the work in place and hand it back
Why: It avoids constructing a new object.
Now a + b changes a, which nothing about the syntax suggests. Writing c = a + b silently destroys a, and a in a loop accumulates results nobody asked for.
Return a new object.
Build the result and return it
Why: Which is what int_to_time does here.
Because that is what the operator means
Why: 3 + 4 does not change the 3.
Operator overloading inherits an expectation from the built-in types: arithmetic operators produce new values and leave their operands alone. A modifying __add__ is legal and violates what every reader assumes.
Faded example
The name is the operator's special method.
Fill in the blanks
def __add__(self, other):
seconds = self.time_to_int() + other.time_to_int()
return int_to_time(seconds)
Why: Defining __add__ is what makes the plus operator work on Time objects, with the left operand as self and the right as other. The body is chapter 16's add_time unchanged — the operator is new syntax for an operation that already existed rather than new logic.
Discrimination
Every operator has one.
Sort into buckets
For each thing you want to work on your class, which method must you define?
Explain it to yourself
The word is doing specific work.
Discussion prompt
Defining __add__ is called operator overloading. What is being overloaded, and what does that word capture?
Hint: How many meanings does the plus sign already have?
Answer:
The operator itself. The plus sign already means several things — integer addition, string concatenation, list joining — so it carries more than one meaning, which is what overloaded describes.
Defining __add__ on your class adds one more meaning, chosen by the type of the left operand. Nothing about the symbol changes; the set of types it works on grows.
Which is why the mechanism is safe: your definition cannot affect how plus behaves on integers or strings, because those types answer for themselves. You are adding a meaning rather than replacing one.
Section
Section 2
Concept
You might also want to add an integer to a Time object. Here is a version of __add__ that checks the type of other and invokes either add_time or increment.
type-based dispatch — A programming pattern that checks the type of an operand and invokes different functions for different types.
# inside class Time:
def __add__(self, other):
if isinstance(other, Time):
return self.add_time(other)
else:
return self.increment(other)| Test | The branch | Note |
|---|---|---|
| isinstance(other, Time) | is it a Time? | True or False |
| if so | add_time | converts both |
| otherwise | increment | assumes a number |
The built-in function isinstance takes a value and a class object, and returns True if the value is an instance of the class. If other is a Time object, __add__ invokes add_time; otherwise it assumes the parameter is a number and invokes increment.
Think Python, 2nd edition — Allen B. Downey §17.7-17.11, pp. 166-166
Picture it
The type of the right operand chooses the branch.
Figure (svg): A flowchart showing an addition dispatching on the type of its right operand
Note the else branch assumes rather than checks — anything that is not a Time is treated as a number, which is a decision worth noticing.
Worked example
One operator, two argument types.
>>> start = Time(9, 45)
>>> duration = Time(1, 35)
>>> print(start + duration)
11:20:00
>>> print(start + 1337)
10:07:17| Right operand | Which branch | Result |
|---|---|---|
| a Time on the right | add_time | 11:20:00 |
| a number on the right | increment | 10:07:17 |
| the same operator | two computations | chosen by type |
Add two Times.
Why: isinstance reports True, so add_time converts both and adds the totals.
Add a number of seconds.
Why: isinstance reports False, so increment adds the seconds to the subject's total.
Note the syntax is identical.
Why: Only the type of the operand differs, and the method chooses what to do about it.
Figure (svg): The state of the program after each line of Worked example both kinds of addition, drawn as a ladder with one rung per traced line
Two additions with one operator. The convenience is real, and the next slide shows what it does not cover.
Verify: Try something that is neither.
Why: start + 'abc' takes the else branch, which assumes a number and fails inside increment with a TypeError about adding a string to an integer — some way from the actual mistake. The else branch assumes rather than checks, so an unexpected type produces a confusing error rather than a clear one.
Prediction
The right operand is a number.
def __add__(self, other):
if isinstance(other, Time):
return self.add_time(other)
else:
return self.increment(other)
print(start + 1337)| Test | Result | Note |
|---|---|---|
| isinstance(1337, Time) | False | not a Time |
| the else branch | increment | assumes a number |
| the result | 10:07:17 | seconds added |
Predict first
What does this print, given start is 09:45:00?
Correct: 10:07:17 — 1337 is not a Time, so the else branch invokes increment.
Why: 1337 seconds is 22 minutes and 17 seconds, which added to 09:45:00 gives 10:07:17. The dispatch chose increment because isinstance reported False — and note that it assumed a number rather than checking, so a string would take the same branch and fail inside increment.
Worked example
A value and a class object.
>>> isinstance(start, Time)
True
>>> isinstance(1337, Time)
False
>>> isinstance(1337, int)
True| Call | What it asks | Result |
|---|---|---|
| a Time and Time | True | it is an instance |
| an integer and Time | False | it is not |
| an integer and int | True | built-in classes work too |
Note the two arguments.
Why: isinstance takes a value and a class object, and returns True if the value is an instance of the class.
Note that the class is not quoted.
Why: It is the class object itself — Time, not 'Time' — unlike hasattr, whose second argument is a string.
Note it works on built-in types too.
Why: isinstance(1337, int) is True, because every object is an instance of some class.
Figure (svg): The state of the program after each line of Worked example the isinstance function, drawn as a ladder with one rung per traced line
A boolean about membership, using the class object. It is the tool lesson 15b introduced for debugging, put to work as a dispatch test.
Verify: Compare with type.
Why: type(start) returns the class object itself and isinstance answers a yes-or-no question about it — so isinstance is what a conditional wants. The distinction matters more once inheritance exists, since isinstance also accepts instances of subclasses.
Trap
__add__ tests for Time and treats everything else as a number.
Handle the two cases you care about
Why: Which is what the book's version does.
A third type — a string, a list, a Point — takes the number branch and fails somewhere inside increment, with a message about the operation rather than about the argument. The error is real and points at the wrong place.
Test for what you accept, and reject the rest clearly.
elif isinstance(other, int): ...
Why: So the third case is identifiable.
else: raise TypeError with a message
Why: Naming the type that was not understood.
The book's version is deliberately compact for teaching. In a class other people use, the difference between I do not accept this and a crash three calls deep is worth four extra lines.
Sorting
The test is whether the right operand is a Time.
Sort into buckets
For each right operand, which branch of __add__ runs?
Faded example
A value and a class object.
Fill in the blanks
def __add__(self, other):
if isinstance(other, Time):
return self.add_time(other)
else:
return self.increment(other)
Why: isinstance takes a value and a class object and returns True if the value is an instance of the class — so the class name is not quoted, unlike hasattr's string argument. Using type(other) == Time would also work here and is less flexible, since isinstance also accepts subclasses.
Explain it
The word describes what the method does.
Discussion prompt
A classmate asks what type-based dispatch means, since the code is just an if statement. Explain the name.
Hint: What is the if statement deciding?
Answer:
It is deciding which computation to send the work to — dispatching it, in the sense of sending something off to be handled elsewhere.
What makes it type-based is the criterion: the decision is made on the type of an operand rather than on its value. Ordinary conditionals branch on what a value is; this one branches on what kind of thing it is.
And the name distinguishes it from the alternative, which is the next idea: writing code that works for several types without asking. The book is explicit that type-based dispatch is useful when it is necessary, and often you can avoid it.
Section
Section 3
Concept
Unfortunately, this implementation of addition is not commutative. If the integer is the first operand, you get an error.
>>> print(start + 1337)
10:07:17
>>> print(1337 + start)
TypeError: unsupported operand type(s) for +: 'int' and 'instance'| Expression | Which object is asked | Result |
|---|---|---|
| Time on the left | Time.__add__ is invoked | works |
| integer on the left | int's add is invoked | and it does not know Times |
| the problem | the wrong object was asked |
The problem is that instead of asking the Time object to add an integer, Python is asking an integer to add a Time object, and it doesn't know how. But there is a clever solution: the special method __radd__, which stands for right-side add.
Think Python, 2nd edition — Allen B. Downey §17.7-17.11, pp. 167-167
Picture it
The left one, always — and that is the whole problem.
Figure (svg): Two columns showing which object the plus operator asks in each ordering
__radd__ is the fallback: when the left operand does not know what to do, the right one is asked instead.
Worked example
Two lines, and the addition becomes commutative.
# inside class Time:
def __radd__(self, other):
return self.__add__(other)
>>> print(1337 + start)
10:07:17| Stage | What happens | Note |
|---|---|---|
| the integer's add | fails | it does not know Times |
| Python then tries | start.__radd__(1337) | the right operand |
| __radd__ | delegates to __add__ | same computation |
Note when it is invoked.
Why: This method is invoked when a Time object appears on the right side of the + operator and the left operand could not handle it.
Note what it does.
Why: It delegates to __add__, because addition of a Time and a number gives the same answer whichever order they are written.
Note the result.
Why: 1337 + start now works and gives the same answer as start + 1337.
Figure (svg): A flowchart showing Python falling back to the right operand's method
Commutative addition, from a two-line method. The delegation works here because the operation genuinely is symmetric.
Verify: Ask when delegating would be wrong.
Why: For subtraction: 5 - t and t - 5 are different questions, so __rsub__ could not simply call __sub__ — it would have to reverse the operands. The one-line delegation is correct only for genuinely commutative operations, which addition is and most operators are not.
Prediction
The integer is on the left, and __radd__ is not defined.
# Time defines __add__ but not __radd__
print(1337 + start)| Step | What happens | Result |
|---|---|---|
| the left operand | an integer | asked first |
| int's add | does not know Times | fails |
| no fallback | __radd__ undefined | TypeError |
Predict first
What happens?
Correct: A TypeError about unsupported operand types — Python asks the integer, which does not know how to add a Time.
Why: Instead of asking the Time object to add an integer, Python is asking an integer to add a Time object. Nothing swaps the operands automatically, which is exactly why __radd__ exists: it gives the right-hand object a chance when the left one cannot help.
Worked example
It names both types, which tells you what happened.
TypeError: unsupported operand type(s) for +: 'int' and 'instance'
# the operator: +
# the left operand: int
# the right operand: your object| Part of the message | What it tells you | Note |
|---|---|---|
| the operator | named first | + |
| the left type | int | the one that was asked |
| the right type | instance | the one that could have helped |
Read the operator.
Why: It names which operation failed, which matters when a line contains several.
Read the left type.
Why: That is the object Python asked, and its inability is the immediate cause.
Read the right type.
Why: If that is your class, the fix is to define the corresponding right-side method.
Figure (svg): The state of the program after each line of Worked example reading the error message, drawn as a ladder with one rung per traced line
A message that identifies the operator and both operand types. When your own class is the second one named, the reversed special method is what is missing.
Verify: Check the message with the operands the other way round.
Why: start + 'abc' gives a different failure — inside increment rather than at the operator — because Time.__add__ accepted the call and then failed on the arithmetic. So the operand-type message means neither object could handle the operation at all, which is a distinct situation from one that tried and failed.
Trap
A class defines __add__ and its author assumes both orderings work.
Take addition to be symmetric
Why: It is for numbers, and the operation being implemented usually is too.
Python asks the left operand, so an integer on the left is asked to add your object and cannot. The failure appears only in the ordering nobody tested, which is usually the less natural one.
Define __radd__ when the reversed ordering is meaningful.
return self.__add__(other)
Why: For a genuinely commutative operation.
And test both orderings
Why: Since only one of them exercises the new method.
The general rule: overloading an operator handles the case where your object is on the left. The reversed method handles the other case, and it is a separate decision because not every operation is symmetric.
Invariant
Two runs of the same expression.
Step through it
What is the same in both runs, and what differs?
The left operand is asked first either way — that never changes. What differs is whether there is anywhere to go when it cannot help, which is the whole of what __radd__ provides.
Faded example
Addition is symmetric, so delegate.
Fill in the blanks
def __radd__(self, other):
return self.__add__(other)
Why: __radd__ is invoked when a Time appears on the right side of the plus operator and the left operand could not handle it, and delegating to __add__ works because addition gives the same answer either way. For subtraction the delegation would be wrong, since t - 5 and 5 - t are different questions.
Socratic
It could ask both and pick.
Discussion prompt
Python asks the left operand and only falls back to the right. Why not ask both?
Hint: What would happen if both knew how?
Answer:
Because both might know, and then something would have to choose between two possibly different answers — which is a decision the language cannot make sensibly.
Asking the left first gives a definite rule: the left operand's meaning wins if it has one, and the right operand's is the fallback. Nothing is ambiguous.
It also matches how the operators already behave. 3 + 4 asks the integer, and 'a' + 'b' asks the string; the left operand has always been the one consulted, and overloading follows the existing rule rather than inventing a new one.
Section
Section 4
Concept
Type-based dispatch is useful when it is necessary, but fortunately it is not always necessary. Often you can avoid it by writing functions that work correctly for arguments with different types.
polymorphic — Pertaining to a function that can work with more than one type.
def histogram(s):
d = dict()
for c in s:
if c not in d:
d[c] = 1
else:
d[c] = d[c]+1
return d
>>> t = ['spam', 'egg', 'spam', 'spam', 'bacon', 'spam']
>>> histogram(t)
{'bacon': 1, 'egg': 1, 'spam': 4}| Aspect | What is true | Note |
|---|---|---|
| written for strings | in chapter 11 | counting letters |
| given a list | works | counting words |
| why | every operation inside works for lists too |
This function also works for lists, tuples, and even dictionaries, as long as the elements of s are hashable, so they can be used as keys in d.
Think Python, 2nd edition — Allen B. Downey §17.7-17.11, pp. 167-168
Picture it
Nothing in histogram mentions strings.
Figure (svg): The histogram function applied to several different argument types
The condition is that the elements be hashable, so they can be used as keys — which is a requirement on the elements rather than on the container.
Worked example
Look at what it actually requires.
for c in s: # s must be iterable
if c not in d: # c must be hashable
d[c] = 1 # likewise| Line | What it requires | Note |
|---|---|---|
| the loop | needs something iterable | string, list, tuple, dict |
| the dictionary key | needs something hashable | the elements |
| nothing else | no type is named |
List the requirements.
Why: The argument must be something a for loop can traverse, and its elements must be usable as dictionary keys.
Note what is not required.
Why: Nothing about strings, or about any particular type — the function never asks what it has been given.
State the general rule.
Why: If all of the operations inside a function work with a given type, the function works with that type.
Figure (svg): The state of the program after each line of Worked example why histogram works so widely, drawn as a ladder with one rung per traced line
Two requirements, both about capabilities rather than types. That is what makes a function polymorphic, and it happened here without being planned.
Verify: Find something it does not work on.
Why: A list of lists fails, because a list is unhashable and cannot be a dictionary key — chapter 11's restriction. So the boundary is exactly the requirement, which confirms the rule: the function works with any type for which its operations work.
Prediction
It was written for strings.
t = ['spam', 'egg', 'spam', 'spam', 'bacon', 'spam']
print(histogram(t))| Requirement | Is it met? | Note |
|---|---|---|
| the loop | a list is iterable | one word per pass |
| the keys | strings are hashable | usable as keys |
| the result | word counts | {'bacon': 1, 'egg': 1, 'spam': 4} |
Predict first
What does this print?
Correct: {'bacon': 1, 'egg': 1, 'spam': 4} — the function works on any iterable whose elements are hashable.
Why: Nothing in histogram mentions strings. It needs something a for loop can traverse and elements usable as dictionary keys, and a list of strings satisfies both — so it counts words rather than characters, with no change to the function. That is polymorphism, and it was not planned.
Worked example
A built-in function, and a class it was never written for.
>>> t1 = Time(7, 43)
>>> t2 = Time(7, 41)
>>> t3 = Time(7, 37)
>>> total = sum([t1, t2, t3])
>>> print(total)
23:01:00| Part | What it needs | Note |
|---|---|---|
| sum | adds the elements of a sequence | with the plus operator |
| Time objects | provide an add method | so plus works |
| the result | a Time | printed with __str__ |
Note what sum requires.
Why: The built-in function sum, which adds the elements of a sequence, works as long as the elements of the sequence support addition.
Note what Time provides.
Why: Since Time objects provide an add method, they work with sum.
Note that nobody arranged this.
Why: sum was written long before your class existed, and works on it because your class supports the operation sum uses.
Figure (svg): A pipeline showing sum adding Time objects using their add method
23:01:00, from a built-in function that has never heard of Times. Polymorphism can facilitate code reuse, and this is what that means concretely.
Verify: Check what sum starts from.
Why: sum begins with 0 and adds each element, so the first addition is 0 + t1 — which needs __radd__, not __add__. That is the previous idea earning its place: without the right-side method, sum would fail on the first element with a message about int and instance.
Trap
A function begins by checking isinstance on its argument, to be safe.
Guard against the wrong type
Why: Which prevents a confusing failure later.
It also rejects every type the function would have worked on. histogram with an isinstance(s, str) guard would refuse the lists and tuples it handles perfectly well, for no benefit.
Let the operations decide.
If all the operations inside work with a type, the function works with that type
Why: Which is the book's own rule.
Use dispatch when the behaviour must differ
Why: Not merely when the types do.
The book's phrasing is careful: type-based dispatch is useful when it is necessary, and often you can avoid it. Necessary means the computation genuinely differs — as it does between adding a Time and adding a number.
Sorting
Iterable, with hashable elements.
Sort into buckets
For each argument, does histogram work?
Faded example
It works because Times support addition.
Fill in the blanks
total = sum([t1, t2, t3])
print(total) # 23:01:00
Why: sum adds the elements of a sequence and works as long as the elements support addition — which Time objects do, because they provide an add method. Note that sum starts from 0, so the first addition is 0 + t1 and needs __radd__ as well as __add__.
Real world
The best kind of polymorphism is the unintentional kind.
Discussion prompt
Think of a tool or a standard that turned out to work for something its designers never considered. What made that possible?
Hint: What did it require, rather than assume?
Answer:
A fitting that works on anything the right size, a file format read by programs that did not exist when it was defined, a socket that powers devices nobody imagined.
What made it possible is that the design specified requirements rather than a list of approved things — anything meeting the requirement works, including things nobody thought of.
That is the book's closing line: the best kind of polymorphism is the unintentional kind, where you discover that a function you already wrote can be applied to a type you never planned for. Writing in terms of operations rather than types is what makes it possible.
Section
Section 5
Concept
Type-based dispatch is useful when it is necessary, but fortunately it is not always necessary. Often you can avoid it by writing functions that work correctly for arguments with different types.
The best kind of polymorphism is the unintentional kind, where you discover that a function you already wrote can be applied to a type you never planned for.
Think Python, 2nd edition — Allen B. Downey §17.7-17.11, pp. 167-168
Picture it
Ask whether the computations differ, not whether the types do.
Figure (svg): A decision flowchart for choosing between dispatch and polymorphism
The default is the right-hand branch, because a function that never asks about types works on more of them.
Worked example
The two computations are not the same.
# adding a Time: convert BOTH and add
seconds = self.time_to_int() + other.time_to_int()
# adding a number: convert ONE and add
seconds = other + self.time_to_int()| Argument | What must happen | Note |
|---|---|---|
| a Time argument | needs converting | it is not a number |
| a number argument | already a number | convert only self |
| the difference | one conversion or two | genuinely different |
Compare the two bodies.
Why: One converts both operands and the other converts only the subject, because the argument is already in the right units.
Note that no single body covers both.
Why: Calling time_to_int on an integer would fail, and treating a Time as a number of seconds would be wrong.
Conclude.
Why: The computations differ, so the dispatch is necessary — which is exactly the condition the book gives.
Figure (svg): Two columns comparing the two computations behind a single overloaded operator
Two different computations behind one operator, which is what makes dispatch the right tool here rather than a failure of imagination.
Verify: Ask whether the difference could be removed.
Why: It could, by converting the argument to a Time first — int_to_time(other) — and then always doing the two-operand version. That would replace the dispatch with a conversion, which is chapter 16's move again: making the general case cover the special one. Whether it is better is a judgement, and noticing the option is the point.
Discrimination
Ask whether the computations differ.
Sort into buckets
For each situation, which approach fits?
Worked example
Polymorphism removes the question rather than answering it.
# with dispatch: ask what it is
if isinstance(other, Time):
...
# without: convert, then proceed
def __add__(self, other):
if not isinstance(other, Time):
other = int_to_time(other)
return int_to_time(self.time_to_int() + other.time_to_int())| Version | How many computations | Note |
|---|---|---|
| the dispatch version | two bodies | one per type |
| the converting version | one body | after normalising |
| what remains | one type check | at the boundary |
Notice where the check moved.
Why: It is still there, at the top, but it normalises rather than branching — so only one computation follows.
Notice the general pattern.
Why: Convert the unusual case into the usual one at the boundary, and the body handles a single type.
Notice it is chapter 16's move.
Why: Making the general case cover the special one, which is what the base-60 conversion did for carrying.
Figure (svg): The state of the program after each line of Worked example the same problem without dispatch, drawn as a ladder with one rung per traced line
One computation instead of two, with the type question confined to a single normalising line. Whether that is better depends on the class, and knowing both shapes is what lets you choose.
Verify: Ask what each version costs when a third type arrives.
Why: The dispatch version needs a third branch; the normalising version needs one more conversion at the boundary and no change to the body. That difference grows with the number of types, which is the practical argument for pushing the checking to the edges.
Trap
Every function that takes an argument begins by checking its type.
Be defensive about inputs
Why: A wrong type does produce confusing failures.
It rejects every type the function would have worked on, which is usually more than the author imagined — and it is the opposite of the polymorphism the book calls the best kind.
Check types when the behaviour must differ.
Dispatch when the computations are genuinely different
Why: As in __add__, where they are.
Otherwise write it once and let the operations decide
Why: If all of the operations inside work with a type, the function works with that type.
The unintentional polymorphism is the payoff, and a type check is what prevents it. histogram would never have worked on lists if its author had guarded it.
Prediction
histogram never mentions a type.
def histogram(s):
d = dict()
for c in s:
...
return d| Line | What it requires | Note |
|---|---|---|
| the for loop | needs an iterable | a capability |
| the dictionary keys | need hashable elements | another |
| no type check | anywhere |
Predict first
What determines which types histogram works on?
Correct: Whether all the operations inside it work with that type — which is the book's own general rule.
Why: The function needs something a for loop can traverse and elements usable as dictionary keys, and any type meeting both works — including a dictionary, which is not a sequence. Nothing was listed or planned, which is why the book calls unintentional polymorphism the best kind.
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 describes the opposite. A polymorphic function works with several types precisely because it does not ask — it uses operations, and any type supporting them works. Adding a type check would refuse types the function handles perfectly well, which is how unintentional polymorphism gets prevented.
Explain it
The book gives a clear condition.
Discussion prompt
A classmate asks whether their function should check what type it was given. Give them the question that decides it.
Hint: What would the check be for?
Answer:
Ask whether the different types need different computations. If they do — as adding a Time and adding a number do — the check is necessary and the branches are the point.
If one computation would work for all of them, the check only refuses types that would have succeeded. histogram works on lists and tuples and dictionaries precisely because nobody guarded it.
The book's rule is the test: if all of the operations inside a function work with a given type, the function works with that type. So write the operations, and let them decide what the function accepts.
Comparison
Fill the blanks. Two answers to what type is this?
Comparison matrix
| Question | Type-based dispatch | Polymorphism |
|---|---|---|
| Does the function ask about types? | yes — isinstance | no — it just uses operations |
| How many computations? | one per type | one, for all of them |
| Which types work? | the ones you listed | every type the operations work for |
| When is it right? | when the computations genuinely differ | the rest of the time |
The book's phrasing is careful: dispatch is useful when it is necessary, and often you can avoid it.
Pattern
Six steps, and the fifth is the one people forget until something fails.
Step 5 is what makes sum work: it starts from 0, so the first addition puts your object on the right, and without __radd__ it fails on the first element.
Python documentation — Classes Classes
Check
One short line.
print(start + duration)| Syntax | Method invoked | Note |
|---|---|---|
| the plus operator | __add__ | on start |
| __str__ | on the result | |
| neither | named in the code |
Check your understanding
How many special methods does this line invoke?
Answer: A
Why: When you apply the + operator to Time objects, Python invokes __add__; when you print the result, Python invokes __str__. As the book says, there is a lot happening behind the scenes — and neither method is named anywhere in the line.
Check
The integer is on the left.
Check your understanding
Why does 1337 + start fail when start + 1337 works?
Answer: A
Why: Instead of asking the Time object to add an integer, Python is asking an integer to add a Time object, and it doesn't know how. The fix is __radd__ — the right-side add — which is invoked when a Time appears on the right and the left operand could not help.
Check
A function written for strings.
Check your understanding
Why does histogram work on a list of words?
Answer: A
Why: The function needs something a for loop can traverse and elements usable as dictionary keys, and a list of strings satisfies both. In general, if all of the operations inside a function work with a given type, the function works with that type — which the book calls the best kind of polymorphism, the unintentional kind.
Real world
Specifying a requirement rather than a list of approved things.
Discussion prompt
Think of a rule written as anything that can do X against one written as a list of allowed items. What happens to each when something new appears?
Hint: A standard against a whitelist.
Answer:
The requirement admits the new thing automatically, if it meets the condition. The list has to be amended, by someone who knows to do it.
Which is the difference between polymorphism and type-based dispatch exactly: one function works on every type supporting its operations, and the other works on the types someone enumerated.
The list is right when the cases genuinely differ and needs maintaining forever. The requirement is right the rest of the time, and it is what makes the unintentional kind possible — a function applied to a type you never planned for.
Commit first
Answer, then rate your confidence.
Predict first
Your class defines __add__ and you write 1337 + t. What happens, and why?
Correct: TypeError — Python asks the left operand, and an integer cannot add your object.
Why: The book puts it exactly: instead of asking the Time object to add an integer, Python is asking an integer to add a Time object, and it doesn't know how. Defining __add__ handles the case where your object is on the LEFT of the operator, and nothing about it applies when the object is on the right. The fix is __radd__, which stands for right-side add and is invoked when your object appears on the right and the left operand could not help — and for a commutative operation it can simply delegate: return self.__add__(other). This matters more than it sounds, because sum starts from 0 and adds each element, so the very first addition is 0 + t1 — with your object on the right. Without __radd__, sum fails on the first element with exactly this error, which is why the polymorphism section's sum example depends on the previous section's fix.
Explain it
One line, and a lot happening behind it.
Discussion prompt
A classmate asks how print(start + duration) can possibly work when neither method is mentioned. Walk them through it.
Hint: Two special methods, invoked by syntax.
Answer:
The plus sign invokes __add__ on the left operand, with the right operand as its argument — so start + duration is start.__add__(duration), which builds and returns a new Time.
Then print invokes __str__ on that result and displays the string it returns, which is the previous lesson's mechanism doing its half.
So two methods they wrote are called without being named, in response to ordinary syntax. That is what makes them special methods, and it is the same mechanism that has always made len and plus work across the built-in types.
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 overloading itself is the previous lesson's mechanism applied to operators, and the thing worth holding is that the left operand becomes self. Dispatch is mechanically simple and the judgement is what matters — the book's condition is that the computations genuinely differ. The right-side add is the piece everyone omits until sum fails on the first element, since sum starts from zero. And polymorphism is the payoff of the whole chapter: writing in terms of operations rather than types is what lets a function you already wrote work on something you never planned for.
Connect it up
One page, from memory.
Draw it
Write one addition expression and draw arrows from each operand to the parameter it fills. Beside it, write the dispatch version of __add__ and mark the condition that makes the dispatch necessary. Underneath, draw the two orderings of a Time and an integer, marking which object Python asks in each and where __radd__ comes in. Finally state the rule for which types a function works on, and give one example of a function that works on more than its author intended.
Recap
Five pages, and chapter 17 is finished: the features earn their keep.
| If you remember one thing | It is this |
|---|---|
| From overloading | For every operator in Python there is a corresponding special method. |
| From the two-method line | print(a + b) invokes __add__ and then __str__, naming neither. |
| From dispatch | Branch when the computation differs, not merely when the type does. |
| From __radd__ | Python asks the left operand. sum starts from 0, so your object is on the right. |
| From polymorphism | The best kind is the unintentional kind. |
The next chapter introduces inheritance — defining a class as a modified version of another — through a case study of card games: Card, Deck and Hand, with class attributes, comparison methods, and the class diagram that shows how the pieces relate.
Think Python, 2nd edition — Allen B. Downey §17.7-17.11, pp. 165-169 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.