This lesson turns functions into methods, covers the two ways to invoke one, explains the self convention and the subject metaphor, and decodes the argument-count error that method syntax produces.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Chapter 17 — Classes and methods
§17.1-17.4, pp. 161-164
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §17.1-17.4, pp. 161-164 — the pages these objectives are drawn from
Warm-up
Look at the Time program from the last chapter.
Discussion prompt
print_time, increment, is_after, time_to_int — list what every one of them has in common, and say what the program does not record about it.
Hint: Look at the first parameter of each.
Answer:
Every one of them takes at least one Time object as an argument. That is not a coincidence — they are all operations on times.
And nothing in the program says so. The class definition and the function definitions sit next to each other with no connection between them beyond the order they happen to be written in.
This chapter makes that connection explicit, by moving the functions inside the class. The transformation is mechanical, and what it buys is that the structure of the program becomes visible.
Concept
The programs from the last two chapters are not really object-oriented because they don't represent the relationships between programmer-defined types and the functions that operate on them. The next step is to transform those functions into methods that make the relationships explicit.
method — A function that is associated with a particular class, and defined inside a class definition.
These features are not strictly necessary; most of them provide alternative syntax for things we have already done. But in many cases the alternative is more concise and more accurately conveys the structure of the program.
Figure (svg): Two columns contrasting functions beside a class with methods inside it
Think Python, 2nd edition — Allen B. Downey §17.1-17.4, pp. 161-162
Section
Section 1
Concept
Python is an object-oriented programming language, which means that it provides features that support object-oriented programming — which has these defining characteristics.
The Time class corresponds to the way people record the time of day, and the functions we defined correspond to the kinds of things people do with times. Similarly, the Point and Rectangle classes correspond to the mathematical concepts of a point and a rectangle.
Think Python, 2nd edition — Allen B. Downey §17.1-17.4, pp. 161-161
Picture it
Each was chosen because it matches something outside the program.
Figure (svg): Three classes from the course paired with what each represents
Which is why the classes have felt natural: they were chosen to match something you already had a concept for.
Worked example
The book is unusually honest about this.
# before: a function beside the class
def print_time(time):
print('%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second))
# after: the same body, indented inside the class
class Time:
def print_time(time):
print('%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second))| Aspect | What changed | Note |
|---|---|---|
| the body | unchanged | character for character |
| the indentation | one level deeper | inside the class |
| what it computes | exactly the same |
Move the definition inside.
Why: To make print_time a method, all we have to do is move the function definition inside the class definition. Notice the change in indentation.
Note that nothing else changed.
Why: The body is identical, and the function computes what it always computed.
Note what the book says about it.
Why: This transformation is purely mechanical; you can do it by following a sequence of steps.
Figure (svg): The state of the program after each line of Worked example what the transformation actually changes, drawn as a ladder with one rung per traced line
One level of indentation. That is genuinely all the transformation is, which is why the chapter can be about what it means rather than how to do it.
Verify: Ask what would break if the class had no other members.
Why: Nothing — a class body containing only a method is complete, exactly as one containing only a docstring was. And the method is available immediately, which shows the relationship is established by position rather than by anything declared.
Prediction
The body is untouched.
class Time:
def print_time(time):
print('%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second))| Aspect | What happened | Note |
|---|---|---|
| the body | identical | to the function version |
| the indentation | one level deeper | inside the class |
| the computation | unchanged |
Predict first
What did moving the definition inside the class change?
Correct: Where it is defined and how it is invoked — not what it computes.
Why: The transformation is purely mechanical and the body is unchanged. What it buys is that the relationship between the class and the function becomes explicit, and that a second, more concise invocation syntax becomes available. The computation is identical.
Worked example
The book's case, stated carefully.
# in Time1.py there is no obvious connection
# between the class definition and the function
# definitions that follow.
# With some examination, it is apparent that
# every function takes at least one Time object
# as an argument.| Aspect | Before | Note |
|---|---|---|
| the connection | exists in fact | every function takes a Time |
| the program | does not say so | you have to examine it |
| methods | say it | by where they are written |
Identify what is missing.
Why: There is no obvious connection between the class definition and the function definitions that follow — the relationship is real and unstated.
Identify how you would find it.
Why: With some examination, it is apparent that every function takes at least one Time object as an argument. That examination is work a reader has to do.
State the benefit precisely.
Why: The alternative is more concise and more accurately conveys the structure of the program.
Figure (svg): Two columns separating what the transformation changes from what it does not
The gain is in what the program communicates rather than in what it computes. The book does not overstate it: these features are not strictly necessary.
Verify: Ask where the benefit becomes concrete.
Why: The book says that sometimes shifting responsibility from the functions onto the objects makes it possible to write more versatile functions, and makes it easier to maintain and reuse code — and that in the examples so far it may not be obvious. So the honest position is that the payoff arrives later, which §17.9's polymorphism section delivers.
Trap
A student concludes that any program not using classes and methods is written wrongly.
Take the chapter's subject as an instruction
Why: The book does say the previous programs are not really object-oriented.
It also says the features are not strictly necessary and that most of them provide alternative syntax for things we have already done. Not really object-oriented is a description, not a criticism.
Read the claim as being about expression.
The alternative more accurately conveys the structure
Why: Which is a real benefit and a specific one.
And the book adds: it is not obvious that it is useful
Why: In the examples seen so far.
Being able to convert between the two forms is the stated goal: if you are comfortable converting from one form to another, you will be able to choose the best form for whatever you are doing. Choosing is the skill, not always picking one.
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 claims a benefit the book never does. The transformation is purely mechanical and the bodies are unchanged, so nothing about the computation differs. The stated benefit is that the alternative is more concise and more accurately conveys the structure of the program.
Faded example
One level of indentation.
Fill in the blanks
class Time:
def print_time(time):
print('%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second))
Why: Moving the definition inside the class body is the entire transformation — the function's body is unchanged. What it establishes is the relationship between the class and the operation, which the previous version left for a reader to notice.
Explain it to yourself
The previous chapters used classes and objects throughout.
Discussion prompt
Chapters 15 and 16 defined classes and created objects. Why does the book say those programs are not really object-oriented?
Hint: Look at the three characteristics.
Answer:
Because of the first characteristic: programs include class AND METHOD definitions. Those chapters had classes and no methods, so the operations sat outside the types they operated on.
The consequence is the second characteristic's absence too — the computation is expressed as functions taking objects rather than as operations on objects, which is a difference in framing rather than in result.
So the phrase is precise rather than dismissive. The programs used objects and did not represent the relationships between the types and the functions operating on them, which is exactly what this chapter adds.
Section
Section 2
Concept
Now there are two ways to call print_time. The first — and less common — way is to use function syntax; the second, and more concise, is to use method syntax.
subject — The object a method is invoked on.
>>> Time.print_time(start)
09:45:00
>>> start.print_time()
09:45:00| Call | What the parts are | Note |
|---|---|---|
| Time.print_time(start) | the class, the method, the object | function syntax |
| start.print_time() | the object, the method | method syntax |
| both | identical output | the same call |
In the first, Time is the name of the class and print_time the name of the method, with start passed as a parameter. In the second, print_time is the name of the method and start is the object the method is invoked on — which is called the subject.
Think Python, 2nd edition — Allen B. Downey §17.1-17.4, pp. 162-163
Picture it
The same three pieces, arranged two ways.
Figure (svg): Two columns showing the same method call written in function syntax and method syntax
Which is more than cosmetic: inside the method, the subject is assigned to the first parameter either way.
Worked example
The first parameter receives it, in both syntaxes.
class Time:
def print_time(time):
print('%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second))
>>> start.print_time()
# start is assigned to time| Part | What happens | Note |
|---|---|---|
| start.print_time() | no arguments in the parentheses | and one parameter |
| the subject | start | assigned to time |
| inside the method | time refers to start | ordinary parameter passing |
Look at the call.
Why: The parentheses are empty, so it looks like a call with no arguments.
Look at the definition.
Why: The method has one parameter, which would be a mismatch if the parentheses told the whole story.
Reconcile them.
Why: Inside the method, the subject is assigned to the first parameter — so start is assigned to time.
Figure (svg): A call diagram showing the subject of a method call being bound to the first parameter
The subject fills the first parameter. That is the single mechanical fact behind every method call, and it explains the argument counting in the next idea.
Verify: Check against the function syntax.
Why: Time.print_time(start) passes start explicitly as the one argument, which lands in the same parameter. So the two syntaxes differ only in whether the subject is written inside the parentheses or before the dot — which makes the mechanism easier to see in the less common form.
Prediction
Two syntaxes for one method.
Time.print_time(start)
start.print_time()| Call | The syntax | Note |
|---|---|---|
| the first | function syntax | start as an argument |
| the second | method syntax | start as the subject |
| both | start is assigned to the first parameter | identical |
Predict first
What is the relationship between these two calls?
Correct: They are the same call written two ways — in both, start is assigned to the first parameter.
Why: In the function syntax Time is the class and start is passed as a parameter; in the method syntax start is the subject and is assigned to the first parameter inside. The book calls the second more concise and more common, and the first is a useful way to see the mechanism, since the subject is written explicitly.
Worked example
The first parameter has a conventional name and a reason for it.
class Time:
def print_time(self):
print('%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second))| Aspect | What is true | Note |
|---|---|---|
| the parameter name | self, by convention | not a keyword |
| what it holds | the subject | the object invoked on |
| why the name | an implicit metaphor | the object is the agent |
Rename the parameter.
Why: By convention, the first parameter of a method is called self, so it would be more common to write print_time this way.
Note it is a convention.
Why: Not a keyword — the earlier version named it time and worked identically. Following the convention is what makes the code readable to anyone else.
Note the metaphor.
Why: The syntax for a function call suggests the function is the active agent; in object-oriented programming, the objects are the active agents.
Figure (svg): The state of the program after each line of Worked example the self convention, drawn as a ladder with one rung per traced line
The same method, with the conventional parameter name. The convention exists because of the metaphor, and the metaphor is what the syntax is expressing.
Verify: Read both call forms aloud.
Why: print_time(start) says: Hey print_time! Here's an object for you to print. start.print_time() says: Hey start! Please print yourself. The book's own gloss — and it makes the reason for the naming convention concrete rather than arbitrary.
Trap
A method is defined as def print_time(): with no parameters, since the call has empty parentheses.
Match the definition to the call
Why: start.print_time() shows no arguments, so none seem needed.
The subject is passed whether or not you declared a parameter for it, so the call raises TypeError: takes 0 positional arguments but 1 was given — for a call with nothing in the parentheses.
Always declare the first parameter.
def print_time(self):
Why: Which receives the subject.
And name it self, by convention
Why: So that anyone reading recognises it immediately.
The empty parentheses at the call site are misleading, and this is the mismatch they cause. Reading the method syntax as the subject plus whatever is in the parentheses makes both the definition and the error message predictable.
Discrimination
Look at what comes before the dot.
Sort into buckets
For each call, which syntax is being used?
Faded example
It receives the subject.
Fill in the blanks
class Time:
def print_time(self):
print('%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second))
Why: By convention the first parameter of a method is called self, and it receives the subject — the object the method was invoked on. It is a convention rather than a keyword, so any name would work; using self is what makes the code readable to everyone else.
Explain it
The name is a convention with a reason behind it.
Discussion prompt
A classmate asks why the first parameter is called self rather than something descriptive like time. Explain the metaphor.
Hint: Read both call forms aloud.
Answer:
The function syntax suggests the function is the active agent: print_time(start) says something like Hey print_time! Here's an object for you to print.
The method syntax reverses that: start.print_time() says Hey start! Please print yourself. The object is the one being addressed, so inside the method it refers to itself — self.
The book is honest that this change in perspective might be more polite and it is not obvious that it is useful, at least in the examples so far. What it eventually buys is versatility, which the polymorphism section delivers.
Section
Section 3
Concept
Here's a version of increment rewritten as a method. This version assumes that time_to_int is written as a method — and note that it is a pure function, not a modifier.
# inside class Time:
def increment(self, seconds):
seconds += self.time_to_int()
return int_to_time(seconds)
>>> end = start.increment(1337)
>>> end.print_time()
10:07:17| Parameter | What it receives | Note |
|---|---|---|
| self | the subject | start |
| seconds | the first written argument | 1337 |
| self.time_to_int() | a method on the subject | chained |
The subject, start, gets assigned to the first parameter, self; the argument, 1337, gets assigned to the second parameter, seconds. Note also that this version is a pure function — it returns a new Time rather than modifying the subject.
Think Python, 2nd edition — Allen B. Downey §17.1-17.4, pp. 163-164
Picture it
One comes from before the dot and the rest from inside the parentheses.
Figure (svg): A call diagram showing the subject and the written argument mapped to two parameters
That is the whole mechanism, and it is why counting arguments at the call site gives one fewer than the method declares.
Worked example
Everyone meets this once, and the explanation is one sentence.
>>> end = start.increment(1337, 460)
TypeError: increment() takes 2 positional arguments but 3 were given| Source | How many | Note |
|---|---|---|
| in the parentheses | two arguments | 1337 and 460 |
| the subject | also an argument | start |
| the total | three | and the method takes two |
Read the message and be confused.
Why: The error message is initially confusing, because there are only two arguments in parentheses.
Count the subject.
Why: But the subject is also considered an argument, so all together that's three.
Recount the method.
Why: increment declares self and seconds, which is two — hence the mismatch.
Figure (svg): A panel decoding the argument-count error message for a method call
Three given and two accepted, because the subject counts. Once you know that, every argument-count error on a method reads correctly.
Verify: Check the opposite mistake.
Why: Defining a method with no parameters and calling it with empty parentheses gives takes 0 positional arguments but 1 was given — the same rule, in the other direction. Both messages are only confusing until you know the subject is counted, and then both say exactly what is wrong.
Prediction
Two in the parentheses.
end = start.increment(1337, 460)
# TypeError: increment() takes 2 positional
# arguments but 3 were given| Source | What it contributes | Count |
|---|---|---|
| the subject | start | counted |
| the written arguments | 1337 and 460 | two more |
| the total | three |
Predict first
Why does the message say three?
Correct: The subject is also considered an argument, so all together that is three.
Why: The message is initially confusing because there are only two arguments in parentheses — but the object before the dot fills the first parameter, so it counts too. Knowing that makes every argument-count error on a method readable: add one to what you can see.
Worked example
The message says positional, and the book explains why.
sketch(parrot, cage, dead=True)
# parrot and cage are POSITIONAL
# dead is a KEYWORD argument| Kind | How it is written | How it is matched |
|---|---|---|
| positional | no parameter name given | matched by position |
| keyword | name=value | matched by name |
| the error message | counts the positional ones | hence the wording |
Define the term.
Why: A positional argument is an argument that doesn't have a parameter name; that is, it is not a keyword argument.
Read the example.
Why: In sketch(parrot, cage, dead=True), parrot and cage are positional, and dead is a keyword argument.
Connect it to the message.
Why: That is why the error says positional arguments rather than just arguments — it is counting the ones matched by position, which includes the subject.
Figure (svg): The state of the program after each line of Worked example positional and keyword arguments, drawn as a ladder with one rung per traced line
Two kinds of argument, and the error message names the kind it is counting. You have already used keyword arguments — print's sep, and sort's reverse.
Verify: Find the keyword arguments you have already used.
Why: print(word, freq, sep='\t') from lesson 13b and t.sort(reverse=True) from the same lesson are both keyword arguments, used before the term was introduced. Recognising them retrospectively is worth a moment, because it shows the feature was familiar before it was named.
Trap
A student writes start.increment(start, 1337), reasoning that the method needs a Time and a number.
Supply every parameter the method declares
Why: It has two, so two arguments look right.
The subject already fills the first parameter, so this passes three things to a method that takes two — and the extra Time lands in seconds, or the count error fires.
Write only the arguments after the first.
start.increment(1337)
Why: The subject fills self; the parentheses fill the rest.
Or use function syntax if you want them all visible
Why: Time.increment(start, 1337) writes both explicitly.
The rule to hold: a method call passes the subject plus whatever is in the parentheses. Counting that way makes both the correct call and the error message obvious.
Sorting
A positional argument has no parameter name.
Sort into buckets
In sketch(parrot, cage, dead=True), and in calls you have seen, which kind is each?
Faded example
The subject is not written in the parentheses.
Fill in the blanks
end = start.increment(1337)
end.print_time() # 10:07:17
Why: The subject start fills the first parameter, self, so only the remaining argument goes in the parentheses. Writing start.increment(start, 1337) would pass three things to a two-parameter method and raise the argument-count error.
Explain it
The commonest first error with methods.
Discussion prompt
A classmate calls a method with what looks like the right number of arguments and gets a count error one higher than they wrote. Explain.
Hint: What is before the dot?
Answer:
The subject counts. The object before the dot is assigned to the first parameter, so a call with two things in the parentheses passes three arguments in total.
Which means the method's definition needs one more parameter than the call appears to supply — conventionally named self, and it comes first.
The rule that makes both readable: a method call passes the subject plus whatever is in the parentheses. Add one to what you can see, and the message says exactly what is wrong.
Section
Section 4
Concept
Rewriting is_after is slightly more complicated because it takes two Time objects as parameters. In this case it is conventional to name the first parameter self and the second parameter other.
# inside class Time:
def is_after(self, other):
return self.time_to_int() > other.time_to_int()
>>> end.is_after(start)
True| Parameter | What it receives | Note |
|---|---|---|
| self | the subject | end |
| other | the written argument | start |
| the comparison | both converted to integers | using the method form |
To use this method, you have to invoke it on one object and pass the other as an argument. One nice thing about this syntax is that it almost reads like English: end is after start?
Think Python, 2nd edition — Allen B. Downey §17.1-17.4, pp. 164-164
Picture it
Subject, verb, object.
Figure (svg): A method call broken into its parts alongside the English sentence it resembles
That readability is the concrete benefit in this example — the function form, is_after(end, start), gives no clue which order the arguments go in.
Worked example
Which is first matters, and the two forms differ in how obvious that is.
# function form: which order?
is_after(end, start)
# method form: reads like English
end.is_after(start)| Form | How clear is the order | Note |
|---|---|---|
| the function form | two arguments of the same type | the order is a convention to remember |
| the method form | subject then object | end is after start |
| the risk | swapping them | much easier in the first |
Look at the function form.
Why: Two Time arguments, and nothing in the call says which is the earlier — you have to remember or check.
Look at the method form.
Why: One nice thing about this syntax is that it almost reads like English: end is after start?
Note what that prevents.
Why: Swapping the arguments produces the opposite answer, silently. The method form makes the swap read wrongly, which is a real defence.
Figure (svg): Two columns comparing the readability of the function form and the method form
The same comparison, with the method form making the argument order legible. This is the more accurately conveys the structure claim made concrete.
Verify: Swap the arguments in both forms and read them.
Why: is_after(start, end) looks perfectly plausible and is wrong; start.is_after(end) reads as start is after end, which is visibly the wrong question. The error is equally easy to make and much easier to see, which is exactly the kind of benefit the book claims.
Prediction
The call reads like a sentence.
# start is 09:45, end is 10:07
print(end.is_after(start))| Part | Its role | Note |
|---|---|---|
| end | the subject | self |
| start | the argument | other |
| the question | is end after start? | True |
Predict first
What does this print?
Correct: True — end is later than start, and the call asks exactly that question.
Why: The subject is what the method is about, so end.is_after(start) asks whether end comes after start. Writing start.is_after(end) would ask the opposite question and print False — which the method syntax makes visible, since it reads as start is after end.
Worked example
Inside a method, the subject is an ordinary object.
def is_after(self, other):
return self.time_to_int() > other.time_to_int()
# self.time_to_int() is an ordinary method call
# whose subject happens to be self| Expression | What it is | Note |
|---|---|---|
| self.time_to_int() | a method call on the subject | nothing special |
| other.time_to_int() | the same method | on the other object |
| both | return integers | then compared |
Note that self is just a name.
Why: It refers to an object, and every operation available on a Time is available through it.
Call a method on it.
Why: self.time_to_int() invokes the method with self as the subject — the same syntax as any other call.
Note the symmetry.
Why: other.time_to_int() does the same thing on the other object, and the two results are compared as ordinary integers.
Figure (svg): The state of the program after each line of Worked example calling a method on self, drawn as a ladder with one rung per traced line
Two conversions and a comparison. The base-60 insight from the previous chapter is doing the work, and the method form just relocates where the conversion lives.
Verify: Compare with the version from chapter 16.
Why: That one built tuples and compared them; this one converts both to integers and compares those. Both are correct, and this version reuses time_to_int rather than restating the field order — which is the reuse the conversion functions were an investment in.
Trap
A method is written as def is_after(self, time2):, describing what the argument is.
Name parameters after their contents
Why: Which is good advice for ordinary functions.
It says nothing a reader did not know — both parameters are Times — and it breaks the convention, so a reader has to work out whether time2 plays the self role or the other role.
Use self and other.
def is_after(self, other):
Why: Which is what the book calls conventional for this case.
And the call site carries the meaning
Why: end.is_after(start) says which is which.
The convention is doing real work here: it tells a reader immediately that the second parameter is another instance of the same class, which a type-based name would not distinguish from any other Time-valued argument.
Faded example
Another instance of the same class.
Fill in the blanks
def is_after(self, other):
return self.time_to_int() > other.time_to_int()
Why: When a method takes a second object of the same class, it is conventional to name the first parameter self and the second other. The convention tells a reader immediately that the argument is another instance rather than an unrelated value, which a type-based name would not.
Comparison
Fill the blanks. The same operation, two ways.
Comparison matrix
| Question | is_after(t1, t2) | t1.is_after(t2) |
|---|---|---|
| Where is it defined? | beside the class | inside the class |
| How many arguments are written? | two | one — the subject is before the dot |
| How obvious is the order? | by convention only | by grammar — it reads like English |
| What does it compute? | the same thing | the same thing |
The bottom row is the honest one: the transformation is purely mechanical, and the benefits are all in the other rows.
Real world
An interface that reads like a sentence.
Discussion prompt
Think of an instruction you have given a machine or a person where the order of two similar things mattered. What made it easy or hard to get right?
Hint: Copy A to B, or move this before that.
Answer:
Copying one file over another, transferring between two accounts, scheduling one thing before another — in each case both arguments are the same kind of thing, and only the order distinguishes them.
What makes it easy is when the phrasing carries the direction: move this into that is harder to get backwards than a form with two identical boxes.
Which is the argument for end.is_after(start) over is_after(end, start). The mistake is equally easy to make and much easier to see, because the wrong version reads as the wrong question.
Section
Section 5
Concept
The exercise is to rewrite time_to_int as a method — and the book adds a warning: you might be tempted to rewrite int_to_time as a method too, but that doesn't really make sense, because there would be no object to invoke it on.
# a method: the subject is the Time
def time_to_int(self):
minutes = self.hour * 60 + self.minute
return minutes * 60 + self.second
# NOT a method: its argument is an integer,
# and it creates the Time
def int_to_time(seconds):
...| Function | Does it have a subject? | Note |
|---|---|---|
| time_to_int | operates on an existing Time | a natural method |
| int_to_time | creates a Time from a number | no subject exists yet |
| the test | is there an object to invoke it on? |
That is the test for whether an operation should be a method: is there an object of the class that the operation is about? For a conversion that produces a Time from an integer, the answer is no.
Think Python, 2nd edition — Allen B. Downey §17.1-17.4, pp. 163-163
Picture it
One conversion starts with a Time and one ends with one.
Figure (svg): Two conversions shown in opposite directions with the subject marked on one
So the two halves of a conversion pair need not be the same kind of thing — which is a genuine asymmetry rather than an inconsistency.
Worked example
The subject supplies the fields.
# inside class Time:
def time_to_int(self):
minutes = self.hour * 60 + self.minute
seconds = minutes * 60 + self.second
return seconds
>>> start.time_to_int()
35100| Part | What changed | Note |
|---|---|---|
| the parameter | self replaces time | the only change |
| the body | self.hour instead of time.hour | mechanical |
| the call | start.time_to_int() | no arguments |
Rename the parameter.
Why: time becomes self, and every reference in the body follows.
Move it inside the class.
Why: Indentation, as before — the transformation is purely mechanical.
Call it on a Time.
Why: start.time_to_int() with empty parentheses, since the subject supplies the only input.
Figure (svg): The state of the program after each line of Worked example time to int as a method, drawn as a ladder with one rung per traced line
35100 seconds for 09:45:00. A method with no arguments beyond its subject, which is the commonest shape for a query.
Verify: Check the number.
Why: Nine hours is 32400 seconds and forty-five minutes is 2700, which sum to 35100. Verifying with a time whose arithmetic you can do in your head is what confirms the conversion rather than merely that it ran.
Discrimination
Ask whether there is an object to invoke it on.
Sort into buckets
For each operation, which form fits?
Worked example
The test is whether a subject exists.
# what would the call even look like?
# 3661.int_to_time() - on an integer?
# Time.int_to_time(3661) - a class-level call
#
# there is no Time to invoke it on, because
# creating one is what the function does| Attempt | Why it fails | Note |
|---|---|---|
| invoking on an integer | the wrong class | int does not have this method |
| invoking on a Time | which Time? | the result does not exist yet |
| the conclusion | a plain function |
Ask what the subject would be.
Why: The input is an integer, which is not a Time — and the Time is what the function produces.
Note the circularity.
Why: There would be no object to invoke it on, because the object is the output rather than the input.
Leave it as a function.
Why: Which is the book's own conclusion, and it is not a failure of the design.
Figure (svg): A decision flowchart for whether an operation should be a method
No sensible subject exists, so it stays a plain function. The next lesson's __init__ addresses the same need from a different direction.
Verify: Ask how the next chapter handles it.
Why: The __init__ method builds a Time from arguments at the moment of creation, which is exactly the job int_to_time does — so the construction case has its own mechanism rather than being forced into a method. Recognising that this gap is filled deliberately makes __init__ read as an answer rather than as new syntax.
Trap
Every function in the module is moved inside the class, since the chapter is about methods.
Apply the transformation uniformly
Why: It is mechanical, so it can be applied to anything.
Functions with no natural subject end up invoked on an arbitrary object, or on the class, and the relationship the method syntax claims to express is not there. The form says something untrue about the code.
Convert the operations that are about an instance.
Ask whether there is an object to invoke it on
Why: Which is the book's own test.
Leave the others as functions
Why: int_to_time is the book's example, and it stays one.
The stated goal is to be able to choose: if you are comfortable converting from one form to another, you will be able to choose the best form for whatever you are doing. Converting everything is not choosing.
Prediction
The subject supplies every field.
# start is 09:45:00
print(start.time_to_int())| Column | Contribution | Value |
|---|---|---|
| hours | 9 x 3600 | 32400 |
| minutes | 45 x 60 | 2700 |
| seconds | 0 | 0 |
Predict first
What does this print?
Correct: 35100 — nine hours is 32400 seconds and forty-five minutes is 2700.
Why: The method converts each field to seconds and adds them, exactly as the function version did — the only change is that the fields come from self rather than from a parameter. Verifying with arithmetic you can do in your head is what confirms the conversion rather than merely that it ran.
Faded example
The subject replaces the parameter.
Fill in the blanks
def time_to_int(self):
minutes = self.hour * 60 + self.minute
return minutes * 60 + self.second
Why: The single parameter receives the subject, and by convention it is named self — so every reference to the Time's fields becomes self.hour and so on. The call is then start.time_to_int(), with empty parentheses, since the subject supplies the only input the method needs.
Socratic
The asymmetry is worth being precise about.
Discussion prompt
time_to_int becomes a method and int_to_time does not. Why is creating an object different in kind from operating on one?
Hint: What does the subject have to be?
Answer:
Because a method's subject is an existing instance, and creation is the process by which an instance comes to exist. There is nothing to be the subject until it is finished.
So a creation operation takes its inputs and produces an object, which is the shape of an ordinary function — whereas a method takes an object and does something with it.
Python does provide a mechanism for the creation case: the __init__ method, which runs at the moment an object is instantiated. That is the next lesson, and it is worth seeing this gap first so that __init__ reads as filling it.
Comparison
Fill the blanks. The book says the difference is syntactic.
Comparison matrix
| Question | A function | A method |
|---|---|---|
| Where is it defined? | at the top level | inside a class definition |
| How is it invoked? | f(obj, ...) | obj.f(...) |
| Where does the first argument come from? | the parentheses | the subject, before the dot |
| What does it compute? | the same thing | the same thing |
Methods are semantically the same as functions, and there are two syntactic differences — which is exactly the two middle rows.
Pattern
Five steps, and the book calls the whole thing purely mechanical.
Step 5 is where the argument-count error comes from. The subject is still an argument — it has just moved outside the parentheses, so a method needs one more parameter than its calls appear to supply.
Python documentation — Classes Classes
Check
Both invoke the same method.
Check your understanding
Which pair of calls does the same thing?
Answer: A
Why: In the function syntax, Time is the class and start is passed as a parameter; in the method syntax, start is the subject and is assigned to the first parameter. Both end with start bound to self, so both produce the same output.
Check
Two things in the parentheses.
start.increment(1337, 460)
# TypeError: increment() takes 2 positional
# arguments but 3 were given| Source | Count | Note |
|---|---|---|
| the subject | start | counted |
| the parentheses | two arguments | 1337, 460 |
| total | three |
Check your understanding
Why does the message say three arguments were given?
Answer: A
Why: The error message is initially confusing because there are only two arguments in parentheses — but the object before the dot fills the first parameter, so all together that is three. Adding one to what you can see makes every such message readable.
Check
One of these has no subject.
Check your understanding
Why does the book say int_to_time should not be rewritten as a method?
Answer: A
Why: Its input is an integer and its output is the Time, so no Time exists to be the subject — creating an object is not an operation on that object. The next chapter's __init__ method handles the construction case with its own mechanism.
Real world
Putting an operation with the thing it operates on.
Discussion prompt
Think of instructions or tools organised by what they act on rather than in one long list. What does that organisation make easier?
Hint: A manual with a chapter per component.
Answer:
A manual with a section per part, tools stored by the job they belong to, settings grouped under the thing they configure — in each case the organisation answers what can I do with this? rather than what operations exist?
Which is the question you actually have when you are holding something. A flat list requires you to know the operation's name before you can find it.
That is what defining methods inside a class buys: the operations on a Time are found by looking at Time, rather than by scanning a module for functions whose first parameter happens to be one.
Commit first
Answer, then rate your confidence. Everyone meets this one.
Predict first
You call start.increment(1337, 460) and get TypeError: increment() takes 2 positional arguments but 3 were given. Why three?
Correct: The subject is also considered an argument, so start, 1337 and 460 make three.
Why: The book flags this because the message is initially confusing: there are only two arguments in parentheses. But method syntax passes the object before the dot as the first argument, so it counts too — and increment declares self and seconds, which is two. The rule worth holding is that a method call passes the subject plus whatever is in the parentheses, so add one to what you can see. The same rule explains the mirror-image error: defining a method with no parameters and calling it with empty parentheses gives takes 0 positional arguments but 1 was given, because the subject arrives whether or not you declared a parameter for it. And positional is in the message because a positional argument is one without a parameter name — as opposed to a keyword argument like sep='\t' or reverse=True, both of which you have already used.
Explain it
The same code, moved and renamed.
Discussion prompt
A classmate asks what actually changed when print_time became a method, since the body is identical. Give them the three differences.
Hint: Where, how it is called, and what it says.
Answer:
Where it is defined: inside the class rather than beside it, which is what makes the relationship between the type and the operation explicit.
How it is invoked: start.print_time() rather than print_time(start), with the object moving from inside the parentheses to before the dot — and being assigned to the first parameter either way.
And what the program says about itself. The book is careful here: these features are not strictly necessary and most provide alternative syntax for things we have already done, but the alternative more accurately conveys the structure of the program.
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 first is worth reading carefully because the book is unusually honest: the features are not strictly necessary, and the benefit is in expression rather than computation. The two syntaxes are one idea — the subject fills the first parameter — and seeing the function form makes the mechanism visible. The argument count is where everyone trips once, and the fix is a single sentence. And the question of what should not be a method is the one that stops the transformation being applied blindly, which the book's int_to_time warning is there to prevent.
Connect it up
One page, from memory.
Draw it
Write one method call in both syntaxes and draw an arrow from the subject to the parameter it fills in each. Beside it, write the argument-count error and annotate where the third argument came from. Then write is_after as a method with its two conventional parameter names, and read the call aloud as a sentence. Finally state the one-question test for whether an operation should be a method, and name the book's example of an operation that fails it.
Recap
Four pages, and the code finally says what it means.
| If you remember one thing | It is this |
|---|---|
| From the characteristics | The features are not strictly necessary; they convey structure. |
| From the transformation | One level of indentation and a renamed parameter. Nothing else. |
| From the two syntaxes | The subject fills the first parameter, wherever it is written. |
| From the error message | Add one to the arguments you can see. |
| From int_to_time | A method needs an object to be invoked on. Creation has none. |
The next lesson introduces the two methods Python calls for you: __init__, which assigns the attributes at the moment an object is created and finally removes the assign-from-outside style, and __str__, which replaces the memory address with something worth reading.
Think Python, 2nd edition — Allen B. Downey §17.1-17.4, pp. 161-164 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.