This lesson introduces the two special methods Python calls for you: __init__, which assigns an object's attributes at the moment it is created, and __str__, which supplies a printable representation.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Chapter 17 — Classes and methods
§17.5-17.6, pp. 164-165
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §17.5-17.6, pp. 164-165 — the pages these objectives are drawn from
Warm-up
Every Time so far has been built the same way.
Discussion prompt
Creating a Time has meant Time() followed by three assignments. List two things that can go wrong with that, and say what they have in common.
Hint: What if you forget one?
Answer:
You can forget an attribute, leaving an object that looks fine until something reads the missing one — and you can misspell a name, which creates a new attribute rather than raising.
What they have in common is that the construction is spread across four statements in the caller's code, so nothing checks it and every call site can get it wrong differently.
This lesson moves that construction inside the class, into a method Python runs at the moment an object is created. One place to get right, and every instance gets it.
Concept
The init method is a special method that gets invoked when an object is instantiated, and __str__ is a special method that is supposed to return a string representation of an object.
# you write these:
def __init__(self, hour=0, minute=0, second=0):
...
def __str__(self):
...
# and Python calls them:
time = Time(9, 45) # calls __init__
print(time) # calls __str__| What you write | What happens | Note |
|---|---|---|
| Time(9, 45) | instantiation | __init__ runs |
| print(time) | printing | __str__ runs |
| neither call | names the method | which is the point |
You never write time.__init__() or time.__str__(). Both are invoked on your behalf, by ordinary syntax that says nothing about them — which is what makes them special methods rather than merely conventional ones.
Figure (svg): Two columns contrasting ordinary methods you call with special methods Python calls
Think Python, 2nd edition — Allen B. Downey §17.5-17.6, pp. 164-165
Section
Section 1
Concept
The init method — short for initialisation — is a special method that gets invoked when an object is instantiated. Its full name is __init__: two underscore characters, followed by init, and then two more underscores.
# inside class Time:
def __init__(self, hour=0, minute=0, second=0):
self.hour = hour
self.minute = minute
self.second = second| Part | What it is | Note |
|---|---|---|
| self | the new object | as in any method |
| hour, minute, second | optional parameters | with defaults of 0 |
| self.hour = hour | stores the parameter as an attribute | same name, different place |
It is common for the parameters of __init__ to have the same names as the attributes. The statement self.hour = hour stores the value of the parameter hour as an attribute of self.
Think Python, 2nd edition — Allen B. Downey §17.5-17.6, pp. 164-165
Picture it
Two things called hour, in two different places.
Figure (svg): A Time object being initialised, showing parameters on the left and attributes on the right
There is no conflict between the two, for the same reason lesson 15a gave: an attribute belongs to an object and is reached through it.
Worked example
The parameters are optional, so four calls all work.
>>> time = Time()
>>> time.print_time()
00:00:00
>>> time = Time(9)
09:00:00
>>> time = Time(9, 45)
09:45:00
# and three arguments override all three defaults| Call | What is supplied | Result |
|---|---|---|
| no arguments | all three defaults | 00:00:00 |
| one argument | overrides hour | 09:00:00 |
| two arguments | override hour and minute | 09:45:00 |
| three | override all three |
Call with nothing.
Why: The parameters are optional, so if you call Time with no arguments you get the default values.
Add arguments from the left.
Why: If you provide one argument, it overrides hour; two override hour and minute.
Note the ordering.
Why: Arguments fill the parameters in order, so you cannot supply a minute without an hour — which is lesson 13b's positional matching.
Figure (svg): The state of the program after each line of Worked example what each number of arguments gives, drawn as a ladder with one rung per traced line
Four usable calls from one definition, because every parameter has a default. This is optional parameters from chapter 13, in the place they are most valuable.
Verify: Check that the defaults produce a valid Time.
Why: Time() gives 00:00:00, which satisfies chapter 16's invariant — every field in range. Choosing defaults that produce a well-formed object matters, because a default of, say, 60 would let Time() build something the rest of the program considers invalid.
Prediction
One argument, three parameters.
def __init__(self, hour=0, minute=0, second=0):
self.hour = hour
self.minute = minute
self.second = second
time = Time(9)
time.print_time()| Parameter | Where its value comes from | Value |
|---|---|---|
| hour | supplied | 9 |
| minute | not supplied | the default, 0 |
| second | not supplied | the default, 0 |
Predict first
What does this print?
Correct: 09:00:00 — one argument overrides hour, and the other two parameters take their defaults.
Why: Arguments fill parameters from the left, so a single argument goes to hour. Option B would require supplying second alone, which positional matching cannot do — you would need a keyword argument, Time(second=9). Nothing raises, because every parameter has a default.
Worked example
Four statements in the caller become one call.
# before
time = Time()
time.hour = 9
time.minute = 45
time.second = 0
# after
time = Time(9, 45)| Version | What the caller writes | Note |
|---|---|---|
| before | four statements | in the caller |
| after | one call | the class does the rest |
| what changed | where the construction lives | and how many places can get it wrong |
Count the statements.
Why: Four in the caller, of which three are assignments that every call site has to repeat.
Move them into the class.
Why: __init__ does the assigning, so a caller supplies values rather than performing the construction.
Note what that fixes.
Why: A forgotten attribute or a misspelled name can now only happen in one place, and it is the place whose job it is.
Figure (svg): Two columns comparing constructing an object by hand with constructing it through __init__
One call in place of four statements, and the attribute list is finally somewhere that runs — chapter 15's docstring promise, kept by code.
Verify: Check that every instance now has every attribute.
Why: Any Time built by calling the class has all three, because __init__ assigns all three unconditionally. That is the property the previous style could not guarantee, and it is what the book means by initialising all of an object's attributes in the init method.
Trap
A student writes t = Time(); t.__init__(9, 45), reasoning that the method has to be invoked somehow.
Call the method you defined
Why: Every other method has to be called explicitly.
It is invoked when an object is instantiated, so Time(9, 45) has already run it — calling it again re-initialises an object that was fine, and reads as though the language needed help.
Pass the arguments to the class.
t = Time(9, 45)
Why: Which calls __init__ with those arguments and self bound to the new object.
And never name the method
Why: The whole point is that ordinary syntax invokes it.
This is the defining property of a special method: Python calls it for you, in response to syntax that does not mention it. Writing the call yourself is legal and defeats the mechanism.
Faded example
Two names, and only one of them belongs to the object.
Fill in the blanks
def __init__(self, hour=0, minute=0, second=0):
self.hour = hour
self.minute = minute
self.second = second
Why: The statement self.hour = hour stores the value of the parameter hour as an attribute of self. It is common for the parameters of __init__ to have the same names as the attributes, and there is no conflict between them because an attribute belongs to an object and is reached through it.
Sorting
Arguments fill parameters from the left.
Sort into buckets
For each call to Time(hour=0, minute=0, second=0), what does it produce?
Explain it to yourself
__init__ would work without them.
Discussion prompt
The book's __init__ gives every parameter a default of zero. What does that buy, and what would be lost without it?
Hint: How many ways can the class then be called?
Answer:
It makes every parameter optional, so one definition supports four call shapes — no arguments through to three — without any extra code.
Without defaults, every caller would have to supply all three, so Time(9) would raise and constructing midnight would mean Time(0, 0, 0).
And the defaults were chosen to produce a valid object: Time() gives 00:00:00, which satisfies chapter 16's invariant. A default that produced a malformed Time would be worse than requiring the argument.
Section
Section 2
Concept
__str__ is a special method, like __init__, that is supposed to return a string representation of an object.
# inside class Time:
def __str__(self):
return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)
>>> time = Time(9, 45)
>>> print(time)
09:45:00| Part | What it does | Note |
|---|---|---|
| __str__ | returns a string | it does not print |
| print(time) | invokes __str__ | and prints what it returns |
| the result | 09:45:00 | instead of an address |
When you print an object, Python invokes the str method. Note the difference from print_time: __str__ returns a string and prints nothing, which is what makes it usable anywhere a string is wanted.
Think Python, 2nd edition — Allen B. Downey §17.5-17.6, pp. 165-165
Picture it
The method supplies the text and print does the printing.
Figure (svg): A pipeline showing print invoking __str__ and displaying the returned string
Which is why the default was so unhelpful: without a __str__, Python falls back on showing the class and the memory address.
Worked example
Almost the same body, and a different contract.
def print_time(self):
print('%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second))
def __str__(self):
return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)| Method | What it does | Note |
|---|---|---|
| print_time | prints | impure: an effect |
| __str__ | returns a string | pure |
| the difference | print against return | one word |
Compare the bodies.
Why: The format expression is identical; one passes it to print and the other returns it.
Note which is pure.
Why: __str__ modifies nothing and has no effect other than returning a value, which makes it pure by chapter 16's definition. print_time is not.
Note what returning buys.
Why: A string can be printed, stored, concatenated, put in a list, or written to a file. Printing can only be printed.
Figure (svg): Two columns contrasting a method that prints with one that returns a string
One word's difference and a much more useful method. The string can go anywhere; the printing can only go to the screen.
Verify: Use the result somewhere other than print.
Why: str(time) returns the string directly, so it can be concatenated or written to a file — none of which print_time allows. That is the practical reason __str__ is the one worth having, and print(time) still covers the common case.
Prediction
The class defines __str__.
def __str__(self):
return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)
time = Time(9, 45)
print(time)| Step | What happens | Result |
|---|---|---|
| print(time) | invokes __str__ | on your behalf |
| __str__ | returns '09:45:00' | a string |
| displays it |
Predict first
What does this print?
Correct: 09:45:00 — when you print an object, Python invokes the str method and displays what it returns.
Why: Option B is what happens without a __str__ method, which is chapter 15's unhelpful default. Option D is what you would get if __str__ printed instead of returning — the text, then None, because the method returned nothing for print to display.
Worked example
The book's own working habit.
# When I write a new class, I almost always
# start by writing __init__, which makes it
# easier to instantiate objects, and __str__,
# which is useful for debugging.| Method | What it buys | Note |
|---|---|---|
| __init__ | easier to instantiate | one call instead of four statements |
| __str__ | useful for debugging | you can see what an object holds |
| together | you can make one and look at it | before anything else works |
Write __init__ first.
Why: It makes it easier to instantiate objects, so every test and every experiment becomes one line.
Write __str__ second.
Why: It is useful for debugging, because printing an object shows what it holds rather than where it lives.
Note what you then have.
Why: The ability to create an instance and inspect it — which is what every subsequent piece of work needs.
Figure (svg): The state of the program after each line of Worked example why these two come first, drawn as a ladder with one rung per traced line
Two methods that make the class usable before any of its real behaviour exists. That is why they are the ones to write first.
Verify: Consider working without them.
Why: Every test needs four statements to build an object and a helper function to see it, and neither is available until you write it. So the alternative is not slightly less convenient but nothing can be checked yet, which is why the habit is worth adopting rather than admiring.
Trap
A student writes __str__ with a print statement in it, since printing is what it seems to be for.
Make the method do the printing
Why: The name suggests display, and print_time worked that way.
It is supposed to RETURN a string representation. Printing inside it means print(time) displays the text and then prints None — because the method returned nothing.
Return the string.
return '%.2d:%.2d:%.2d' % (...)
Why: One word different from print_time, and it is the important word.
Let print do the printing
Why: Which is what it was going to do anyway.
The symptom is distinctive: the right text, followed by None on the next line. That combination always means a function printed instead of returning, and something else printed its return value.
Discrimination
Some syntax calls it and some does not.
Sort into buckets
For each expression, is __str__ invoked?
Faded example
One word different from print_time.
Fill in the blanks
def __str__(self):
return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)
Why: __str__ is supposed to return a string representation of an object, and print does the displaying. Writing print here instead would show the text and then print None, because the method would return nothing for print to display — which is the distinctive symptom of this mistake.
Explain it
The right text, and something extra.
Discussion prompt
A classmate's print(time) shows 09:45:00 and then None on the next line. Diagnose it.
Hint: What did __str__ return?
Answer:
Their __str__ prints instead of returning. The text appears because the method printed it, and then None appears because print displayed the method's return value — which was nothing.
__str__ is supposed to return a string representation, and printing is print's job. Changing print to return fixes it.
The combination is a reliable tell: correct output followed by None means a function printed where it should have returned, and something else printed the result. It is the mirror image of lesson 6a's void-function trap.
Section
Section 3
Concept
Both of these methods share a pattern: you define them, and something else invokes them. You never write the call.
time = Time(9, 45) # calls __init__
print(time) # calls __str__
# what you never write:
# time.__init__(9, 45)
# print(time.__str__())| Syntax | Which method it invokes | Note |
|---|---|---|
| instantiation | invokes __init__ | the class call |
| printing | invokes __str__ | the print call |
| the names | never appear | in ordinary code |
The double underscores mark a method the language itself knows about. Defining one changes what ordinary syntax does with your objects, which is the whole mechanism behind the operator overloading of the next lesson.
Think Python, 2nd edition — Allen B. Downey §17.5-17.6, pp. 164-165
Picture it
Each piece of ordinary syntax has a method behind it.
Figure (svg): Pieces of ordinary syntax paired with the special methods they invoke
Which explains something from earlier: len works on a string and a list and a dictionary because each of those types defines the method len looks for.
Worked example
This mechanism was there all along.
len('abc') # 3
len([1, 2, 3]) # 3
len({'a': 1}) # 1
# three different types, one function -
# because each type defines the method
# that len invokes| Call | What it counts | Note |
|---|---|---|
| len on a string | counts characters | the string's own method |
| len on a list | counts elements | the list's |
| len on a dictionary | counts items | the dictionary's |
Notice one function and three behaviours.
Why: len gives a sensible answer for every sequence type, which cannot be one implementation.
Explain it with special methods.
Why: Each type defines the method len invokes, so len asks the object rather than deciding for itself.
Connect it to your own classes.
Why: Defining that method on a class of your own would make len work on your objects too, by the same mechanism.
Figure (svg): A flowchart showing a built-in function delegating to the object's special method
The built-ins delegate to the object. That is why they have always worked across types, and why they can be made to work on yours.
Verify: Check what happens on a type that lacks the method.
Why: len(5) raises TypeError: object of type 'int' has no len() — which is the mechanism failing visibly. The error message names the type rather than the function, because the function did its job and the object could not answer.
Prediction
Nothing in the call names it.
time = Time(9, 45)| Part | What happens | Note |
|---|---|---|
| the class call | creates an object | and initialises it |
| __init__ | invoked automatically | with self bound to the new object |
| the arguments | passed through | 9 and 45 |
Predict first
What invokes the __init__ method here?
Correct: Instantiation — the init method gets invoked when an object is instantiated.
Why: Calling the class creates the object and runs __init__ with the arguments you supplied, which is why you never write time.__init__(9, 45). That automatic invocation in response to ordinary syntax is exactly what makes a method special rather than merely conventional.
Worked example
Two underscores on each side, and nothing else.
__init__ # correct
_init_ # not special - just a method with a name
__init # not special
__str__ # correct
__string__ # not a method Python looks for| Name | Special? | Note |
|---|---|---|
| two underscores each side | the convention | and the exact name matters |
| a near miss | an ordinary method | never invoked automatically |
| the symptom | your method is ignored | silently |
Get the underscores right.
Why: Two underscore characters, followed by init, and then two more underscores — four in total, and a single one on either side is not the same name.
Get the name right.
Why: Python looks for specific names; __string__ is not one of them, however reasonable it looks.
Note the failure mode.
Why: A misspelled special method is a perfectly legal ordinary method that nothing ever calls, so nothing raises and the behaviour simply does not appear.
Figure (svg): The state of the program after each line of Worked example the naming convention, drawn as a ladder with one rung per traced line
Exact names, and near misses fail silently. That is worth knowing before you spend time wondering why print still shows an address.
Verify: Check by calling the method explicitly.
Why: If print(t) shows an address but t.__str__() returns the right string, the method exists and Python is not finding it — which almost always means the name is wrong. That two-line check separates my method is broken from my method is not being called.
Trap
A class defines _init_ with single underscores and its objects come out with no attributes.
Approximate the name
Why: The double underscores are easy to miscount, especially in a proportional font.
It becomes an ordinary method that nothing calls. Instantiation succeeds, every attribute is missing, and the first read raises AttributeError somewhere unrelated.
Type the name exactly.
Two underscores, the name, two underscores
Why: __init__ and __str__, with four underscores each.
And test the effect, not the method
Why: Create an object and print it; if the special method is being found, that works.
The failure is silent because a method with the wrong name is still a valid method. Nothing in the language can tell that you meant it to be special, which is why the check has to be that the behaviour appears.
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 of what makes a method special. When you print an object, Python invokes the str method — you never write print(time.__str__()). Being invoked by ordinary syntax that does not name it is the defining property, and it is the mechanism the next lesson's operator overloading uses.
Faded example
Two underscores on each side.
Fill in the blanks
def __init__(self, hour=0, minute=0, second=0):
self.hour = hour
self.minute = minute
self.second = second
Why: Two underscore characters, followed by init, and then two more underscores. A near miss like _init_ is a perfectly legal ordinary method that nothing ever calls, so instantiation succeeds with no attributes assigned and the failure appears later as an AttributeError.
Real world
Conventions that a system acts on.
Discussion prompt
Think of a place where naming something a particular way makes a system treat it differently. What happens when the name is slightly wrong?
Hint: A file extension, a required filename.
Answer:
A file extension that decides which program opens it; a configuration file the system only reads under one exact name; a folder whose name makes it appear in a menu.
A near miss usually fails silently, because the thing is still a valid file — it is simply not the one the system was looking for, so nothing happens and nothing complains.
Which is exactly the misspelled special method: a legal method that nothing invokes. The general lesson is that when behaviour is triggered by a name, the failure mode is absence rather than error — so the check has to be that the behaviour appeared.
Section
Section 4
Concept
It is legal to add attributes to objects at any point in the execution of a program, but if you have objects with the same type that don't have the same attributes, it is easy to make mistakes. It is considered a good idea to initialise all of an object's attributes in the init method.
# every Time has all three, always
def __init__(self, hour=0, minute=0, second=0):
self.hour = hour
self.minute = minute
self.second = second| Aspect | What is true | Note |
|---|---|---|
| all three assigned | unconditionally | every instance |
| the guarantee | same type, same attributes | no variation |
| what that prevents | a missing attribute | in some instances only |
The problem is not that adding attributes later is illegal — it is that objects of one type with different attributes are hard to reason about, since any function taking one has to cope with several shapes.
Think Python, 2nd edition — Allen B. Downey §17.5-17.6, pp. 168-168
Picture it
Legal, and hard to work with.
Figure (svg): Two columns contrasting instances with a uniform set of attributes and instances that differ
This is lesson 12c's shape error, arriving inside a single class — where you would least expect the objects to differ.
Worked example
vars and getattr, for when you have to look.
>>> p = Point(3, 4)
>>> vars(p)
{'y': 4, 'x': 3}
def print_attributes(obj):
for attr in vars(obj):
print(attr, getattr(obj, attr))| Expression | What it gives | Note |
|---|---|---|
| vars(obj) | a dictionary of names to values | the object's attributes |
| for attr in vars(obj) | the names, as strings | looping over the keys |
| getattr(obj, attr) | the value for that name | the name as a string |
Look at the whole object.
Why: vars takes an object and returns a dictionary that maps from attribute names — as strings — to their values.
Loop over it.
Why: The keys are the attribute names, so iterating gives them one at a time.
Fetch each value.
Why: getattr takes an object and an attribute name as a string and returns the attribute's value — which is how you read a name you have as text.
Figure (svg): The dictionary vars returns for a Point object
A function worth keeping handy for debugging, which prints every attribute and its value. It is the general version of the vars check from lesson 15b.
Verify: Use it to catch the typo from chapter 15.
Why: An object with x and Y shows both in vars, which makes a capitalisation slip visible immediately. That is why the tool is worth having: an AttributeError tells you what is missing, and vars tells you what is there instead.
Prediction
An object with two attributes.
p = Point(3, 4)
print(vars(p))| Part | What it is | Note |
|---|---|---|
| vars | the object's attributes | as a dictionary |
| the keys | names, as strings | 'x' and 'y' |
| the values | the attribute values | 3 and 4 |
Predict first
What does this print?
Correct: {'y': 4, 'x': 3} — vars returns a dictionary mapping attribute names, as strings, to their values.
Why: The order is unpredictable, as for any dictionary, which is why the book's own output shows y before x. Option B is what looping over that dictionary would give you one at a time — the names alone — and getattr is how you turn each name back into a value.
Worked example
Every function has to cope with each shape.
# if some Times have a 'label' attribute and some do not:
def describe(t):
if hasattr(t, 'label'):
return t.label + ' ' + str(t)
return str(t)
# every function touching label needs this check| Approach | What it costs | Note |
|---|---|---|
| the check | hasattr in every function | repeated |
| the alternative | initialise label in __init__ | even to None |
| then | one shape, no checks |
Note the cost of variation.
Why: Any function that might touch the optional attribute has to check for it, and the check has to be repeated everywhere.
Note the alternative.
Why: Initialising it in __init__ — even to None — gives every instance the same shape, and the check becomes a comparison against None if it is needed at all.
Note what the book recommends.
Why: It is considered a good idea to initialise all of an object's attributes in the init method.
Figure (svg): The state of the program after each line of Worked example what varying shapes cost, drawn as a ladder with one rung per traced line
One check per function, or one assignment in __init__. The second is one line and removes the question everywhere.
Verify: Ask when hasattr is still needed.
Why: When the objects genuinely come from elsewhere and their shape is not under your control — which lesson 15b's try approach also covers. For a class you wrote, initialising in __init__ makes the question disappear, so hasattr is a tool for foreign objects rather than your own.
Trap
A program attaches an extra attribute to some Time objects when a particular calculation needs one.
Add what you need where you need it
Why: It is legal to add attributes at any point in the execution of a program.
Now objects with the same type do not have the same attributes, so every later function has to guess which kind it received — and the ones that guess wrong fail with an AttributeError far from the cause.
Initialise every attribute in __init__.
Including ones that are usually empty
Why: self.label = None gives every instance the attribute.
So every function can rely on the shape
Why: And a check, if needed, becomes a comparison rather than a hasattr.
The legality is not the question. The book's phrasing is precise: it is legal, and if you have objects with the same type that don't have the same attributes, it is easy to make mistakes.
Faded example
The name arrives as a string.
Fill in the blanks
def print_attributes(obj):
for attr in vars(obj):
print(attr, getattr(obj, attr))
Why: Looping over vars gives the attribute names as strings, and getattr takes an object and an attribute name as a string and returns the attribute's value. Writing obj.attr instead would look for an attribute literally named attr, which almost certainly does not exist.
Discrimination
Three ways to interrogate an object.
Sort into buckets
For each question, which tool answers it?
Socratic
Adding attributes later is legal.
Discussion prompt
Python lets you add an attribute to one instance and not another. Why is that worth avoiding even though it works?
Hint: What does a function receiving one have to assume?
Answer:
Because a function taking a Time can no longer rely on what a Time has. It either checks every attribute it touches, or it assumes and fails on the instances that differ.
And the failure lands in the function that reads the missing attribute, which may be far from whichever code path produced the odd object — the cause-and-symptom distance again.
So the recommendation is about reasoning rather than legality: initialising all of an object's attributes in the init method means every instance of a type has one shape, and every function can be written once.
Section
Section 5
Concept
One of the goals of object-oriented design is to make software more maintainable, which means that you can keep the program working when other parts of the system change, and modify the program to meet new requirements.
# the interface: what the class provides
# time_to_int, is_after, add_time, __str__
# the implementation: how it stores things
# hour, minute, second
# or: a single integer, seconds since midnight| Part | What it is | Note |
|---|---|---|
| the interface | the methods a class provides | what callers use |
| the implementation | how the attributes are represented | a private decision |
| the principle | the first should not depend on the second |
A design principle that helps achieve that goal is to keep interfaces separate from implementations. For objects, that means the methods a class provides should not depend on how the attributes are represented.
Think Python, 2nd edition — Allen B. Downey §17.5-17.6, pp. 169-169
Picture it
The same methods, storing the time two ways.
Figure (svg): Two columns showing two possible implementations behind the same set of methods
If the interface was designed carefully, changing between these two is invisible to everything using the class.
Worked example
The book's own example of a different implementation.
# instead of three attributes:
# self.seconds = 35100
def is_after(self, other):
return self.seconds > other.seconds # easier
def __str__(self):
minutes, second = divmod(self.seconds, 60)
hour, minute = divmod(minutes, 60)
return '%.2d:%.2d:%.2d' % (hour, minute, second) # harder| Method | Effect of the change | Note |
|---|---|---|
| is_after | one comparison | easier than before |
| __str__ | two divmods first | harder than before |
| the interface | unchanged | callers see no difference |
Consider the alternative.
Why: We could replace these attributes with a single integer representing the number of seconds since midnight.
Note the trade.
Why: This implementation would make some methods, like is_after, easier to write, but it makes other methods harder.
Note what does not change.
Why: The methods the class provides — their names, their arguments, what they return — which is the interface.
Figure (svg): The state of the program after each line of Worked example the alternative representation, drawn as a ladder with one rung per traced line
A genuine trade between the two representations, with no clear winner. What matters is that choosing differently would change no calling code.
Verify: Check what a caller would notice.
Why: Nothing: t.is_after(other) and print(t) behave identically under both. The only way to tell them apart is to read the attributes directly — which is exactly what callers should not be doing, and why the principle is worth following.
Sorting
One is what callers use; the other is how it works.
Sort into buckets
For each, which side of the line does it belong to?
Worked example
The cost of getting it wrong is paid later.
# code that uses the interface: survives a change
if t1.is_after(t2):
...
# code that reaches into the implementation: does not
if t1.hour > t2.hour or (t1.hour == t2.hour and ...):
...| Style | What happens on a change | Note |
|---|---|---|
| using methods | unaffected by the representation | safe |
| reading attributes | breaks if they change | coupled |
| the discipline | callers use the interface |
Note the situation.
Why: After you deploy a new class, you might discover a better implementation.
Note the risk.
Why: If other parts of the program are using your class, it might be time-consuming and error-prone to change the interface.
Note the payoff.
Why: If you designed the interface carefully, you can change the implementation without changing the interface, which means other parts of the program don't have to change.
Figure (svg): A flowchart showing which calling code survives a change of implementation
Code that goes through the methods survives a change of representation; code that reads the attributes does not. The discipline costs a little now and saves a great deal later.
Verify: Ask what makes an interface careful.
Why: Methods that express what a caller wants rather than how the data are stored. is_after is careful; a method named compare_hours would not be, because its name commits to a representation and would become a lie the moment that changed.
Trap
Code outside the class compares two Times by reading t1.hour and t2.hour directly.
Use the data that are there
Why: The attributes are accessible, so using them looks natural.
Every such use depends on the representation, so changing to seconds-since-midnight breaks all of them — and they are scattered, so some will be missed.
Go through the interface.
t1.is_after(t2)
Why: Which says what you want rather than how it is stored.
And add a method if the interface lacks one
Why: That is the right response to needing something the class does not offer.
The methods a class provides should not depend on how the attributes are represented — and the other half of that is that callers should not either. Both halves are needed for the separation to be worth anything.
Prediction
The class switches to storing seconds since midnight.
# caller A
if t1.is_after(t2): ...
# caller B
if t1.hour > t2.hour: ...| Caller | What it depends on | Note |
|---|---|---|
| caller A | uses a method | the interface |
| caller B | reads an attribute | the implementation |
| after the change | A works, B breaks |
Predict first
Which caller keeps working?
Correct: A, because it uses a method rather than an attribute.
Why: If you designed the interface carefully, you can change the implementation without changing the interface, which means other parts of the program don't have to change. Caller B depends on the attribute hour existing, which the new representation does not provide — and such uses are scattered, so some will be missed.
Faded example
Say what you want, not how it is stored.
Fill in the blanks
if t1.is_after(t2):
print('later')
Why: The method expresses the question and hides the representation, so it keeps working if the class switches from three attributes to a single integer. Comparing t1.hour with t2.hour would depend on the implementation and would also be wrong on its own, since equal hours need the minutes compared too.
Real world
Something whose insides changed without your noticing.
Discussion prompt
Think of something you use whose internals were replaced without any change to how you use it. What made that possible?
Hint: A service, a device, a part.
Answer:
A payment system that changed its provider, a device whose components were replaced, a service whose storage moved — in each case what you interacted with stayed the same.
What made it possible is that the interaction was defined in terms of what you wanted rather than how it worked: a plug, a form, a button, none of which mention the internals.
Which is the principle exactly: the methods a class provides should not depend on how the attributes are represented. Where that discipline is missing, changing the insides means changing everyone who touches it.
Comparison
Fill the blanks. Both are called for you, by different syntax.
Comparison matrix
| Question | __init__ | __str__ |
|---|---|---|
| What invokes it? | instantiation — calling the class | printing, or str() |
| What does it return? | nothing — it assigns attributes | a string |
| What does it buy? | easier instantiation | useful debugging output |
| When to write it? | first, when starting a new class | second, immediately after |
The book's own habit: almost always start by writing __init__ and __str__, before any of the class's real behaviour.
Pattern
Six steps, and the first two are the book's own opening move.
Step 5 is why the first two come first: until both exist, you cannot cheaply make an object or see what it holds, so nothing else can be checked.
Python documentation — Classes Classes
Check
Every parameter has a default.
def __init__(self, hour=0, minute=0, second=0):
self.hour = hour
self.minute = minute
self.second = second
print(Time(9, 45))| Parameter | Source | Value |
|---|---|---|
| hour | supplied | 9 |
| minute | supplied | 45 |
| second | the default | 0 |
Check your understanding
What does this print, given a __str__ that formats hour:minute:second?
Answer: A
Why: Two arguments override hour and minute, and second takes its default of zero. The parameters are optional, so nothing raises — and printing the object invokes __str__, which formats all three fields with leading zeros.
Check
One word decides it.
Check your understanding
A __str__ method that prints instead of returning produces what output for print(time)?
Answer: A
Why: The method prints the text itself, and then print displays the method's return value — which is None, since it returned nothing. That combination is the reliable tell for this mistake: correct output followed by None.
Check
The class switches to a single integer.
Check your understanding
What does keeping the interface separate from the implementation mean for a class?
Answer: A
Why: That separation is what lets you change the implementation without changing the interface, so other parts of the program don't have to change — which matters because you might discover a better implementation after the class is already in use.
Real world
Setting something up completely at the moment it is created.
Discussion prompt
Think of a process that fills in everything at the start against one that adds details later. What goes wrong with the second?
Hint: A form completed in stages.
Answer:
Records created with some fields blank and filled in later; an account set up partially and completed on a second visit; equipment assembled in stages.
What goes wrong is that anything encountering the thing in between has to cope with an incomplete version — so every process downstream needs a check, and the ones that forget fail on exactly the incomplete cases.
Which is the argument for initialising all of an object's attributes in the init method. It is not that adding them later is forbidden; it is that objects of one type with different shapes make every function harder to write correctly.
Commit first
Answer, then rate your confidence.
Predict first
What invokes the __str__ method?
Correct: Printing the object, or converting it with str — you never call it yourself.
Why: When you print an object, Python invokes the str method, and displays the string it returns. That automatic invocation in response to ordinary syntax is exactly what makes it a special method — the same property that makes __init__ run when an object is instantiated, without anyone writing time.__init__(9, 45). Two practical consequences follow. First, __str__ must RETURN the string rather than print it; a version that prints gives you the right text followed by None, because print then displays the method's missing return value. Second, the name must be exact — two underscores, str, two underscores — since a near miss like __string__ is a perfectly legal ordinary method that nothing ever invokes, so the behaviour simply never appears and nothing raises. And this same mechanism is what the next lesson builds on: for every operator in Python there is a corresponding special method, so defining __add__ makes the plus sign work on your objects.
Explain it
Two methods, and neither is ever called by name.
Discussion prompt
A classmate asks why __init__ and __str__ have those strange names and where they get called from. Explain both.
Hint: What syntax triggers each?
Answer:
The double underscores mark a method the language itself knows about. Python looks for those exact names, so __init__ runs when an object is instantiated and __str__ when one is printed.
Which means you never write the calls: Time(9, 45) invokes __init__ with the arguments you gave, and print(time) invokes __str__ and displays what it returns.
The practical warning is that a near miss fails silently. _init_ or __string__ is a legal ordinary method that nothing calls, so your objects come out with no attributes or still print as an address — and nothing raises to tell you why.
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 init method is mechanically simple, and the part worth practising is the default arguments — four call shapes from one definition. The return-versus-print distinction has a distinctive symptom, correct output followed by None, which is worth recognising on sight. The special-method mechanism is the one that generalises: understanding that ordinary syntax invokes a named method is what makes the next lesson's operator overloading obvious rather than magical. And the interface principle is the chapter's design advice, which costs nothing now and saves a great deal when a representation changes.
Connect it up
One page, from memory.
Draw it
Write __init__ and __str__ for the Time class from memory, and beside each write the ordinary syntax that invokes it. Underneath, list the four calls from Time() to Time(9, 45, 30) with what each produces. Then draw a line down the page with the interface on one side and the implementation on the other, and put the Time class's methods and attributes on the correct sides.
Recap
Two pages, and the two methods to write first in any new class.
| If you remember one thing | It is this |
|---|---|
| From __init__ | Construction moves into the class, so there is one place to get it right. |
| From __str__ | Return the string. Printing it gives you the text and then None. |
| From special methods | Ordinary syntax invokes them, and a misspelled name fails silently. |
| From initialisation | Every instance of a type should have the same attributes. |
| From the design principle | Callers use methods, so the representation stays free to change. |
The next lesson uses the same mechanism to make operators work on your objects: __add__ for the plus sign, type-based dispatch for adding either a Time or a number, __radd__ for when the object is on the right, and polymorphism — where a function you already wrote turns out to work on a type you never planned for.
Think Python, 2nd edition — Allen B. Downey §17.5-17.6, pp. 164-165 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.