This lesson defines a new type with the class statement, creates instances, assigns and reads attributes with dot notation, and works through the design decision of which attributes a class should have.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Chapter 15 — Classes and objects
§15.1-15.3, pp. 147-149
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §15.1-15.3, pp. 147-149 — the pages these objectives are drawn from
Warm-up
You already have two of them.
Discussion prompt
You need to represent the point (3, 4) in a program. Name two ways to do it with what you already know, and say what is awkward about each when you have a hundred points.
Hint: Two variables, or one sequence.
Answer:
Two separate variables, x and y — which does not survive being passed to a function or stored in a list, since the pair is only connected by your intentions.
Or a tuple or list of two elements, which travels as one thing — but then the coordinates are positions rather than names, and p[0] tells a reader nothing.
The third option is to create a new type, whose parts have names. It is more complicated than the other two, and this chapter is about the advantages that buys.
Concept
At this point you know how to use functions to organise code and built-in types to organise data. The next step is object-oriented programming, which uses programmer-defined types to organise both.
class — A programmer-defined type. A class definition creates a new class object.
There are several ways we might represent a point: two separate variables, elements of a list or tuple, or a new type. Creating a new type is more complicated than the other options, but it has advantages that will be apparent soon.
Figure (svg): Two columns comparing a tuple with a programmer-defined type for the same data
Think Python, 2nd edition — Allen B. Downey §15.1-15.3, pp. 147-147
Section
Section 1
Concept
We have used many of Python's built-in types; now we are going to define a new one. As an example, we will create a type called Point that represents a point in two-dimensional space.
class Point:
"""Represents a point in 2-D space."""| Part | What it does | Note |
|---|---|---|
| the header | names the new class | Point |
| the body | a docstring | explaining what it is for |
| the result | a class object | which nothing has used yet |
The header indicates that the new class is called Point, and the body is a docstring that explains what the class is for. You can define variables and methods inside a class definition — the book gets back to that later.
Think Python, 2nd edition — Allen B. Downey §15.1-15.3, pp. 147-148
Picture it
The class itself is a value, like everything else.
Figure (svg): A pipeline showing a class statement producing a class object which produces instances
Two different things share the name in conversation: the class Point, and each Point you make from it. Keeping them apart is most of what this idea is about.
Worked example
Printing each shows what kind of thing it is.
>>> Point
<class '__main__.Point'>
>>> blank = Point()
>>> blank
<__main__.Point object at 0xb7e9d3ac>| Expression | What it is | Note |
|---|---|---|
| Point | the class object | printed as <class ...> |
| Point() | calling it | creates an instance |
| blank | one instance | printed with its address |
Look at the class.
Why: Defining a class named Point creates a class object, and because Point is defined at the top level, its full name is __main__.Point.
Call it.
Why: To create a Point, you call Point as if it were a function. The return value is a reference to a Point object.
Look at the instance.
Why: When you print an instance, Python tells you what class it belongs to and where it is stored in memory — the prefix 0x means the number is in hexadecimal.
Figure (svg): The state of the program after each line of Worked example the class object and an instance, drawn as a ladder with one rung per traced line
Two different printed forms: one says class and one says object. That word is the reliable way to tell which you are holding.
Verify: Make a second instance and compare.
Why: Point() again gives a different address, so the two instances are separate objects — while Point itself is one object shared by all of them. Checking that two instances are not identical confirms the factory analogy: each call produces something new.
Prediction
The class is called like a function.
class Point:
"""Represents a point in 2-D space."""
blank = Point()
print(type(blank))| Step | What happens | Result |
|---|---|---|
| Point() | instantiation | a new object |
| blank | an instance of Point | |
| type(blank) | the class it belongs to | <class '__main__.Point'> |
Predict first
What does this print?
Correct: <class '__main__.Point'> — type reports the class an instance belongs to.
Why: Option B is what printing blank itself would give: an instance shows its class and its address. type reports the class object instead. And nothing raises for an empty class body — a docstring is a complete body, and calling the class is how you create instances of it.
Worked example
Three words for closely related things.
blank = Point() # instantiation
# blank is an INSTANCE of the class Point
# every object is an instance of some class| Term | What it means | Note |
|---|---|---|
| instantiation | creating a new object | calling the class |
| instance | the object created | of that class |
| object | the same thing | the words are interchangeable |
Name the act.
Why: Creating a new object is called instantiation, and the object is an instance of the class.
Note the interchangeability.
Why: Every object is an instance of some class, so object and instance are interchangeable.
Note the book's convention.
Why: In this chapter Downey uses instance to indicate that he is talking about a programmer-defined type.
Figure (svg): A class object shown with two separate instances created from it
Three terms describing one situation. The distinction that matters is between the class and its instances, not between object and instance.
Verify: Check the claim that every object is an instance.
Why: type(3) reports <class 'int'> and type('a') reports <class 'str'> — so the integers and strings you have used all along are instances of built-in classes. That makes this chapter's new type a member of the same system rather than a special case, which is worth noticing.
Trap
A student writes Point.x = 3.0, expecting to set the coordinate.
Use the name you defined
Why: Point is the thing that was just created, so it looks like the object to work with.
That sets an attribute on the class object, which every instance then appears to share. Nothing raises, and the value shows up on Points that were never given a coordinate.
Make an instance first, and assign to that.
blank = Point(), then blank.x = 3.0
Why: The class is the factory; the instance is the thing with coordinates.
Read the printed form when unsure
Why: <class ...> is the factory; <... object at ...> is a product.
The confusion is natural because both are objects and both take dot notation. The distinction is that one of them is shared by everything the factory has ever made.
Discrimination
One is a factory and the others are its products.
Sort into buckets
For each, which are you holding?
Faded example
A header and a body.
Fill in the blanks
class Point:
"""Represents a point in 2-D space."""
Why: The class statement creates a new class object with the given name, and the body here is just a docstring explaining what the class is for. A docstring is a complete body — nothing else is required at this stage, and methods come two chapters later.
Explain it to yourself
Defining a class creates a value, which is not obvious.
Discussion prompt
Defining a class named Point creates a class object. What does it mean for the class itself to be an object, and where have you seen the consequence?
Hint: What can you do with any object?
Answer:
It means the class is a value like any other: it has a name bound to it, it can be printed, it can be passed to a function, and it can be stored in a data structure.
You have already seen the consequence without noticing — type(x) returns a class, and comparing types is comparing class objects. Printing one shows <class ...>, which is just how that kind of object displays.
And it explains why calling Point() works at all: calling is something you do to an object, and this particular kind of object responds by making an instance. The class object is like a factory, and the factory is itself a thing you can hold.
Section
Section 2
Concept
You can assign values to an instance using dot notation. This syntax is similar to selecting a variable from a module, such as math.pi — but in this case we are assigning values to named elements of an object, which are called attributes.
attribute — One of the named values associated with an object.
>>> blank.x = 3.0
>>> blank.y = 4.0
>>> blank.y
4.0
>>> x = blank.x
>>> x
3.0| Statement | What it does | Note |
|---|---|---|
| blank.x = 3.0 | creates the attribute | and gives it a value |
| blank.y | reads it | 4.0 |
| x = blank.x | copies the value | into an ordinary variable |
As a noun, AT-trib-ute is pronounced with emphasis on the first syllable, as opposed to a-TRIB-ute, which is a verb.
Think Python, 2nd edition — Allen B. Downey §15.1-15.3, pp. 148-148
Picture it
A state diagram that shows an object and its attributes.
Figure (svg): An object diagram showing a Point instance with x and y attributes
Each attribute refers to a floating-point number — the same reference picture as chapter 10, with names instead of positions.
Worked example
Dot notation is an expression like any other.
>>> '(%g, %g)' % (blank.x, blank.y)
'(3.0, 4.0)'
>>> distance = math.sqrt(blank.x**2 + blank.y**2)
>>> distance
5.0| Usage | What happens | Result |
|---|---|---|
| blank.x | go to the object and get x | 3.0 |
| in a format expression | an ordinary value | no special handling |
| in arithmetic | likewise | 5.0 |
Read the expression aloud.
Why: blank.x means: go to the object blank refers to and get the value of x.
Use it anywhere.
Why: You can use dot notation as part of any expression — a format operand, an argument, a term in arithmetic.
Note there is no conflict.
Why: Assigning blank.x to a variable named x is fine: there is no conflict between the variable x and the attribute x, because they live in different places.
Figure (svg): The state of the program after each line of Worked example reading an attribute in an expression, drawn as a ladder with one rung per traced line
The formatted string and the distance, computed from the attributes as ordinary values. Nothing about dot notation restricts where it can appear.
Verify: Check the distance by hand.
Why: A point at (3, 4) is 5 units from the origin, by the three-four-five right triangle — so the computed 5.0 confirms both the attribute reads and the arithmetic. Choosing a case whose answer you know is what makes the check worth running.
Prediction
A variable and an attribute share a name.
blank.x = 3.0
x = 99
print(blank.x, x)| Name | Where it lives | Value |
|---|---|---|
| blank.x | an attribute of the object | 3.0 |
| x | a variable | 99 |
| the two | in different places | no conflict |
Predict first
What does this print?
Correct: 3.0 99 — there is no conflict between the variable x and the attribute x.
Why: An attribute belongs to an object and is reached through it, so blank.x and x are two entirely separate things that happen to share a spelling. The book says so explicitly, because the coincidence looks alarming — and it means you can freely name a variable after the attribute you took it from.
Worked example
An ordinary argument, and an alias.
def print_point(p):
print('(%g, %g)' % (p.x, p.y))
>>> print_point(blank)
(3.0, 4.0)| Part | What happens | Note |
|---|---|---|
| print_point(blank) | passes a reference | as always |
| p | an alias for blank | one object, two names |
| p.x | the same attribute | as blank.x |
Take an instance as a parameter.
Why: You can pass an instance as an argument in the usual way — nothing special is needed.
Read its attributes inside.
Why: print_point takes a point as an argument and displays it in mathematical notation.
Note the aliasing.
Why: Inside the function, p is an alias for blank, so if the function modifies p, blank changes.
Figure (svg): A state diagram showing a parameter aliasing the caller's Point object
The point printed in mathematical notation. And the aliasing warning is chapter 10's list-arguments rule, now applying to a programmer-defined type.
Verify: Ask which kind of object this makes a Point.
Why: A mutable one: its attributes can be changed after it is created, so passing it to a function carries the same risk as passing a list. Nothing in the class statement decided that — it follows from attributes being assignable, which is the default.
Trap
A program creates a Point and immediately reads p.x.
Assume the class defines its attributes
Why: The docstring lists them, so they look declared.
The docstring is documentation, not a definition. Nothing exists until something assigns it, so reading p.x raises AttributeError on a fresh instance.
Assign before you read.
blank = Point(), then blank.x = 3.0
Why: Which is exactly what the book's example does.
And expect this to improve later
Why: The next chapter introduces __init__, which assigns the attributes when the object is created.
At this stage a class is a name and a docstring, and every attribute is created by an assignment from outside. That is deliberately minimal, and it is why the __init__ method feels like such an improvement when it arrives.
Faded example
Dot notation works inside any expression.
Fill in the blanks
def print_point(p):
print('(%g, %g)' % (p.x, p.y))
Why: Dot notation reads an attribute anywhere an expression is allowed, including inside a tuple being matched to format sequences. The expression p.y means: go to the object p refers to and get the value of y.
Sorting
The same syntax, three different things behind it.
Sort into buckets
For each expression, what is the dot selecting?
Explain it
Two numbers either way.
Discussion prompt
A classmate asks why they would use a Point rather than the tuple (3.0, 4.0), which they already know how to make. Give them two reasons.
Hint: What does p[0] tell a reader?
Answer:
The parts have names. p.x says what it is, where p[0] says only that it is the first of something — and in a longer program that difference compounds.
And the type is distinguishable. A Point is recognisably a Point, whereas any two-element tuple looks like any other, so nothing tells you whether you are holding a coordinate, a pair of scores, or a name and an age.
The book adds that creating a new type is more complicated than the alternatives, and that the advantages become apparent soon. The big one is still ahead: an object is somewhere to attach behaviour, which is what the next two chapters are about.
Section
Section 3
Concept
Sometimes it is obvious what the attributes of an object should be, but other times you have to make decisions. Imagine you are designing a class to represent rectangles: what attributes would you use to specify the location and size?
At this point it is hard to say whether either is better than the other, so the book implements the first one, just as an example. You can ignore angle — to keep things simple, assume the rectangle is either vertical or horizontal.
Think Python, 2nd edition — Allen B. Downey §15.1-15.3, pp. 149-149
Picture it
The same shape, described two ways.
Figure (svg): Two columns comparing corner-plus-size with two-corners as rectangle attributes
The way to choose is the same as chapter 13's: think about the operations you will need, and pick the representation that makes them straightforward.
Worked example
A docstring that lists the attributes, since nothing else does.
class Rectangle:
"""Represents a rectangle.
attributes: width, height, corner.
"""| Attribute | What it holds | Note |
|---|---|---|
| width, height | numbers | the size |
| corner | a Point object | the lower-left corner |
| the docstring | the only record of this | nothing enforces it |
Write the class.
Why: The header names it and the body is a docstring, exactly as for Point.
List the attributes in the docstring.
Why: width and height are numbers; corner is a Point object that specifies the lower-left corner.
Note what the listing is worth.
Why: It is documentation and nothing more — no attribute exists until it is assigned, and nothing checks that they match the docstring.
Figure (svg): The state of the program after each line of Worked example the Rectangle class, drawn as a ladder with one rung per traced line
A class whose attributes are described rather than declared. At this stage the docstring is the only place the design is recorded, which is why it is worth writing.
Verify: Ask what would happen if the docstring were wrong.
Why: Nothing at all — the program would work and the documentation would mislead. That makes the docstring a promise you have to keep by hand, which is a real weakness of this minimal style and part of why __init__ is an improvement: it puts the attribute list in code that runs.
Prediction
Two attribute selections in one expression.
box.corner = Point()
box.corner.x = 5.0
print(box.corner.x)| Part | What it selects | Note |
|---|---|---|
| box.corner | the embedded Point | step one |
| .x | an attribute of that Point | step two |
| the result | 5.0 |
Predict first
What does box.corner.x mean?
Correct: Go to box, select corner, then go to that object and select x — two separate steps.
Why: The book spells this out because reading it as one operation hides the requirement that the first step must find something. Without box.corner = Point() beforehand, the first step raises AttributeError and the second never happens. There is no limit on how many dots an expression may chain.
Worked example
One of the attributes is itself an object.
box = Rectangle()
box.width = 100.0
box.height = 200.0
box.corner = Point()
box.corner.x = 0.0
box.corner.y = 0.0| Attribute | What it holds | Note |
|---|---|---|
| box.width | a number | directly on the rectangle |
| box.corner | a Point object | embedded |
| box.corner.x | two dots | into the embedded object |
Create and fill the rectangle.
Why: To represent a rectangle you have to instantiate a Rectangle object and assign values to the attributes.
Create the corner separately.
Why: box.corner = Point() makes a Point and attaches it, and its own attributes are assigned afterwards.
Read the double dot.
Why: The expression box.corner.x means: go to the object box refers to and select the attribute named corner; then go to that object and select the attribute named x.
Figure (svg): An object diagram showing a Rectangle whose corner attribute is an embedded Point
A rectangle holding a point. An object that is an attribute of another object is embedded — which is the book's figure 15.2.
Verify: Check what box.corner alone gives you.
Why: The Point object itself, printed as <...Point object at 0x...>. So the two dots are two separate steps, and stopping after the first leaves you holding the embedded object — which is exactly what you want if you mean to pass it to print_point.
Trap
A program writes box.corner.x = 0.0 without first assigning box.corner.
Set the coordinate directly
Why: The double dot reads like one path to one place.
It is two steps, and the first fails: box has no attribute corner, so the read raises AttributeError before the assignment to x is even considered.
Create the inner object first.
box.corner = Point()
Why: Which is a line the book's example has, and it is easy to skip.
Then box.corner.x = 0.0
Why: Now the first step succeeds and the second assigns.
Reading the expression as two steps makes the requirement obvious: go to box and select corner, THEN go to that object and select x. The first step has to find something.
Comparison
Fill the blanks. Each makes different operations easy.
Comparison matrix
| Question | corner, width, height | two opposing corners |
|---|---|---|
| How many objects? | one Point and two numbers | two Points |
| Changing the width | assign to one attribute | move one corner, computing the new position |
| Finding the width | read the attribute | subtract the two x coordinates |
| Which does the book implement? | this one, just as an example | not implemented |
Each makes some operations direct and others a small calculation, which is why the book says it is hard to say whether either is better.
Faded example
The inner object has to exist first.
Fill in the blanks
box = Rectangle()
box.corner = Point()
box.corner.x = 0.0
Why: box.corner.x is two steps, and the first has to find an object. Assigning box.corner = Point() creates one and attaches it, so the next line can go to it and set x. Without this line the read of box.corner raises AttributeError.
Socratic
The book declines to decide. You need not.
Discussion prompt
You have to pick between corner-plus-size and two-corners for a real program. What would settle it?
Hint: Chapter 13 asked the same kind of question.
Answer:
The operations you will need — exactly the reasoning of chapter 13's data structure selection. Resizing and moving are direct with corner-plus-size; testing whether a point is inside is direct with two corners.
And which invariants you want. Two corners allows a rectangle where the first corner is above and to the right of the second, so every function has to cope with that or normalise it; corner-plus-size makes negative dimensions the equivalent problem.
So the answer is the same as it was for suffixes: list the operations, see which representation makes them straightforward, and if it is genuinely close, implement the easier one and find out. That is why the book picks one just as an example.
Section
Section 4
Concept
You can pass an instance as an argument in the usual way. Inside the function, the parameter is an alias for the object the caller passed — so if the function modifies it, the caller sees the change.
def move_right(p, dx):
p.x = p.x + dx # modifies the caller's Point
>>> blank.x
3.0
>>> move_right(blank, 2.0)
>>> blank.x
5.0| Part | What happens | Note |
|---|---|---|
| the call | passes a reference | not a copy |
| p.x = ... | modifies the object | through the parameter |
| blank.x | sees the change | one object, two names |
Inside the function, p is an alias for blank, so if the function modifies p, blank changes. This is chapter 10's rule about list arguments, applying to a programmer-defined type for exactly the same reason.
Think Python, 2nd edition — Allen B. Downey §15.1-15.3, pp. 149-149
Picture it
Two frames, one object.
Figure (svg): A state diagram showing a function parameter and the caller's variable referring to one Point
Nothing about defining a class changed the rules. An instance is a mutable object, so all of chapter 10's warnings apply unchanged.
Worked example
The same design decision as lesson 10c.
# modifies the caller's point
def move_right(p, dx):
p.x = p.x + dx
# returns a new point, leaving the caller's alone
def moved_right(p, dx):
q = Point()
q.x = p.x + dx
q.y = p.y
return q| Function | Its contract | Note |
|---|---|---|
| move_right | modifies, returns None | the caller's point changes |
| moved_right | builds a new Point | the caller's is untouched |
| the call sites | differ visibly | one assigns, one does not |
Recognise the choice.
Why: It is the same one from lesson 10c: modify what you were given, or return something new.
Note what the modifying version costs.
Why: The caller's object changes, which is efficient and must be documented — a surprise otherwise.
Note what the returning version costs.
Why: Building a new instance and copying every attribute, which is more code and cannot surprise anyone.
Figure (svg): Two columns contrasting a function that modifies a Point with one that returns a new one
Two contracts, exactly as for lists. Instances are mutable, so both are available and the caller has to know which they are calling.
Verify: Check the returning version copies every attribute.
Why: It sets q.y as well as q.x — omitting it would leave the new Point without a y at all, and the failure would appear later as an AttributeError somewhere else. That is a real weakness of building objects attribute by attribute, and the next chapter's __init__ removes it.
Prediction
The function assigns to an attribute.
def move(p, dx):
p.x = p.x + dx
blank = Point()
blank.x = 3.0
move(blank, 2.0)
print(blank.x)| Step | What happens | Result |
|---|---|---|
| the call | p is an alias for blank | one object |
| p.x = ... | modifies the object | not the name |
| blank.x | the same object | 5.0 |
Predict first
What does this print?
Correct: 5.0 — inside the function, p is an alias for blank, so modifying p modifies blank.
Why: Assigning to an attribute modifies the object rather than repointing a name, so the change is visible through every reference to it. Writing p = Point() inside the function instead would repoint the local parameter and leave the caller's Point untouched — lesson 10c's distinction, unchanged.
Worked example
Everything you know about objects still applies.
p = Point()
p.x = 1.0
q = p # aliasing, not copying
q.x = 99.0
print(p.x) # 99.0
# and a Point cannot be a dictionary key by value:
# two Points with equal attributes are different objects| Situation | What happens | Note |
|---|---|---|
| q = p | copies the reference | one object |
| q.x = 99.0 | modifies it | p.x sees the change |
| two equal Points | still two objects | == compares identity by default |
Note the aliasing.
Why: q = p copies the reference rather than the object, exactly as for a list — so both names refer to one Point.
Note the mutability.
Why: Attributes can be assigned after creation, which is what makes an instance mutable.
Note what does not come free.
Why: Two Points with the same coordinates are different objects, and comparing them with == reports False by default.
Figure (svg): The state of the program after each line of Worked example an instance is not special, drawn as a ladder with one rung per traced line
An instance behaves like every other mutable object. Nothing about defining a class gives you copying, comparison or printing — those come later, and their absence is why this chapter's Points are awkward to work with.
Verify: Compare two Points with the same coordinates.
Why: p == q is False even when every attribute matches, because the default comparison asks whether they are the same object. That is chapter 10's equivalent-against-identical distinction, and here the equivalence test simply does not exist yet — which is one of the gaps the next chapters fill.
Trap
A function returns the Point it was given, and the caller treats the result as a new object.
Read a return value as something fresh
Why: Functions that return usually build something.
If the function returns its argument, the caller now has a second name for the same object — so modifying the result modifies the original, which is exactly what the caller was trying to avoid.
Build a new instance when you mean a copy.
q = Point(), then assign each attribute
Why: Which is what makes it a genuinely separate object.
Verify with is if it matters
Why: q is p should be False.
Chapter 10's slice trick has no equivalent here — there is no t[:] for an instance at this stage. The next lesson covers the copy module, which does the job properly.
Sorting
Ask whether the object or the name is being changed.
Sort into buckets
For each function body, does the caller see a difference?
Faded example
A copy has to be constructed.
Fill in the blanks
def moved_right(p, dx):
q = Point()
q.x = p.x + dx
q.y = p.y
return q
Why: Calling the class creates a new instance, so q refers to a different object from p and modifying it cannot affect the caller. Writing q = p instead would alias rather than copy, and every assignment to q would change the caller's Point — which is the bug this function exists to avoid.
Explain it to yourself
A new type, and the same rules.
Discussion prompt
Explain why everything you learned about aliasing and mutability in chapter 10 applies unchanged to a class you defined yourself.
Hint: What made lists dangerous?
Answer:
Because the rules were never about lists. They were about mutable objects: a name refers to an object, assignment copies the reference, and modifying the object is visible through every reference to it.
An instance is a mutable object — its attributes can be assigned after creation — so all three statements apply word for word, with attribute in place of element.
Which is worth noticing as a general property of the language: defining a new type does not create a new set of rules. It creates a new kind of thing that the existing rules already cover, which is why this chapter can be so short.
Section
Section 5
Concept
At this stage a class is a name and a docstring, and every attribute is created by an assignment from outside. That is enough to be useful and it leaves several obvious things undone.
Each of these is fixed in the next two chapters, and knowing that they are gaps rather than facts about objects makes the fixes easier to appreciate.
Think Python, 2nd edition — Allen B. Downey §15.1-15.3, pp. 148-149
Picture it
Everything awkward here has an answer coming.
Figure (svg): Two columns pairing each limitation of the minimal class style with its eventual fix
Downey introduces classes this way deliberately: with nothing hidden, it is clear that an object is just a thing with named parts.
Worked example
Nothing checks the names.
>>> blank = Point()
>>> blank.x = 3.0
>>> blank.Y = 4.0 # a typo: capital Y
>>> blank.y
AttributeError: 'Point' object has no attribute 'y'| Statement | What happens | Note |
|---|---|---|
| blank.Y = 4.0 | creates an attribute named Y | legal |
| blank.y | no such attribute | AttributeError |
| the docstring | says nothing about it | and cannot |
Note that assignment creates.
Why: Any attribute name may be assigned, and doing so creates it — there is no list of permitted names.
Note when the error appears.
Why: Not at the typo, which is a perfectly legal assignment, but at the later read of the name that was intended.
Note the distance between them.
Why: The two may be far apart, and the object in between looks fine — it simply has an attribute nobody meant to create.
Figure (svg): A panel showing a typo creating an unintended attribute and the later failure it causes
A legal assignment and a later AttributeError, with nothing connecting them. This is the cause-and-symptom distance the whole course keeps meeting.
Verify: Look at the object's attributes when diagnosing.
Why: vars(blank) shows {'x': 3.0, 'Y': 4.0}, which makes the typo obvious immediately. Listing what an object actually has is the fastest diagnosis for an AttributeError, because the mistake is nearly always a name that was created rather than one that is missing.
Prediction
Nothing has been assigned yet.
blank = Point()
print(blank.x)| Step | What happens | Result |
|---|---|---|
| Point() | an instance with no attributes | the class defines none |
| blank.x | nothing to find | AttributeError |
| the docstring | documentation only | creates nothing |
Predict first
What happens?
Correct: An AttributeError — attributes come into being when they are assigned, and nothing has assigned x.
Why: The class body is a docstring, which creates nothing. There are no defaults and no declarations, so a fresh instance has no attributes at all. The next chapter's __init__ method fixes exactly this, by assigning the attributes at the moment the object is created.
Worked example
The address is not what you wanted.
>>> blank
<__main__.Point object at 0xb7e9d3ac>
>>> print_point(blank)
(3.0, 4.0)| Approach | What you see | Note |
|---|---|---|
| printing directly | class and address | no contents |
| a helper function | the contents | written by hand |
| the eventual fix | a __str__ method | two chapters away |
Print the instance.
Why: Python tells you what class it belongs to and where it is stored in memory.
Notice what is missing.
Why: The coordinates, which are the only thing anyone wants to see.
Write a function instead.
Why: print_point takes a point and displays it in mathematical notation, which is the whole of the workaround at this stage.
Figure (svg): The state of the program after each line of Worked example what printing an instance gives you, drawn as a ladder with one rung per traced line
An address, and a hand-written function to get anything better. The address is genuinely useful for one thing — telling two instances apart — and useless for everything else.
Verify: Use the address for what it is good for.
Why: Printing two instances shows different addresses, which confirms they are separate objects — the same information is is gives. So the default display is a debugging aid about identity rather than a failed attempt at showing contents.
Trap
A student reads the docstring's attribute list as a declaration and expects a misspelled attribute to be rejected.
Read the class as a definition of shape
Why: Which is what a class means in several other languages.
In Python at this stage the class body is a docstring, and attributes come into being when they are assigned. Nothing is declared and nothing is checked.
Treat the docstring as a promise you keep by hand.
List the attributes there anyway
Why: Which is what the book's Rectangle does, and it is the only record.
And assign them all in one place
Why: Which the next chapter's __init__ makes possible.
The looseness is deliberate: it makes the mechanism visible. An object is a thing with named parts, and the parts appear when you put them there — which is worth seeing plainly before the conveniences arrive.
Error analysis
Mark each and say what happens.
Annotate
Three of the four are fixed by the next chapter's __init__ and __str__ methods.
Comparison
Fill the blanks. Each gap has a specific fix.
Comparison matrix
| Question | Now | Later |
|---|---|---|
| Who assigns the attributes? | code outside the class | an __init__ method |
| What does printing show? | the class and a memory address | whatever __str__ returns |
| How are two instances compared? | by identity | by __eq__, if you define one |
| Where does behaviour live? | in separate functions like print_point | in methods inside the class |
The bottom row is the biggest change, and it is what object-oriented actually refers to.
Real world
The advantage of a named part over a numbered one.
Discussion prompt
Think of a form or a record where fields are identified by position rather than by name. What goes wrong?
Hint: A CSV file with no header row.
Answer:
A spreadsheet without headers, a data file where column four is the date, a function taking six positional arguments — in each case the meaning lives in someone's memory rather than in the data.
What goes wrong is that a change of order is undetectable. Insert a column and every reader is silently wrong, because nothing in the data says what column four now is.
Named parts make the meaning travel with the value. p.x cannot be silently reinterpreted the way p[0] can, and that is the concrete advantage of a Point over a tuple — before any of the object-oriented machinery arrives at all.
Comparison
Fill the blanks. Both hold two numbers.
Comparison matrix
| Question | (3.0, 4.0) | a Point |
|---|---|---|
| How are the parts identified? | by position | by name |
| Mutable? | no | yes — attributes can be assigned |
| Can it be a dictionary key? | yes | yes, but compared by identity |
| Is the type distinguishable? | no — any pair looks the same | yes — a Point is recognisably a Point |
The second row is the one to watch: making a new type gave up immutability, so chapter 10's aliasing rules now apply.
Pattern
Six steps, in the minimal style this chapter uses.
Step 4 is where every attribute comes from at this stage, and step 2's list is a promise you keep by hand — a misspelling creates a new attribute rather than raising.
Python documentation — Classes Classes
Check
Two things, both printed.
Check your understanding
Which of these is printed as <class '__main__.Point'>?
Answer: A
Why: Defining a class named Point creates a class object, whose printed form begins with the word class. An instance prints instead as <__main__.Point object at 0x...>, showing the class it belongs to and where it is stored in memory.
Check
One expression, two steps.
box.corner.x = 0.0| Part | What it does | Note |
|---|---|---|
| box.corner | select corner from box | step one |
| .x | select x from that object | step two |
| the requirement | corner must already exist |
Check your understanding
What must be true for this line to work?
Answer: A
Why: The expression means: go to the object box refers to and select the attribute named corner; then go to that object and select x. The first step is a read, so corner must already exist — which is why the book's example writes box.corner = Point() before this line.
Check
The function assigns to an attribute.
Check your understanding
A function takes a Point and sets p.y = 0.0. What does the caller see?
Answer: A
Why: Inside the function, the parameter is an alias for the caller's object, so if the function modifies it, the caller's Point changes. Assigning to an attribute modifies the object rather than repointing a name — chapter 10's rule, applying to an instance for the same reason it applied to a list.
Real world
Deciding what a thing's parts are is a modelling decision, not a fact.
Discussion prompt
Think of something you would have to describe with a fixed set of fields — an address, a booking, a recipe. What did you have to decide rather than discover?
Hint: Is a full name one field or two?
Answer:
Whether a name is one field or two; whether an address has a fixed number of lines; whether a date and a time are one thing or separate. None of these is settled by the world.
And each decision makes some operations easy and others awkward, exactly like the rectangle's corner-plus-size against two-corners. Sorting by surname is easy with two fields and hard with one.
Which is why the book presents the rectangle choice without resolving it: the right answer depends on what you will do with the object, and knowing that the question exists is more useful than any particular answer.
Commit first
Answer, then rate your confidence.
Predict first
You write blank = Point() and then print(blank.x). What happens?
Correct: An AttributeError — attributes come into being when they are assigned, and nothing has assigned x.
Why: This is the thing to understand about the minimal class style the chapter uses. The class body is a docstring, which declares nothing and creates nothing; a fresh instance has no attributes at all. Even a docstring that lists the attributes — as the Rectangle class does — is documentation that nothing enforces, which is why a misspelled name creates a new attribute rather than raising. The diagnostic is vars(blank), which lists what the object actually has and usually makes the mistake obvious. The next chapter's __init__ method fixes this properly by assigning the attributes at the moment the object is created, so that every instance has them from the start.
Explain it
The class, and the things it makes.
Discussion prompt
A classmate is confused about the difference between Point and blank. Give them the distinction and a way to check which they are holding.
Hint: One is a factory.
Answer:
Point is the class object — the factory. There is exactly one of it, and defining the class is what created it.
blank is an instance: something the factory made. Every call to Point() produces a new one, at a different address, and they are all separate objects.
The check is to print it. <class '__main__.Point'> is the factory; <__main__.Point object at 0x...> is a product. And type(blank) returns the factory, which is another way of asking the same question.
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 class-against-instance distinction is the one worth being pedantic about now, because everything later assumes it. Attributes are mechanically simple, and the surprise is how little the class does — nothing is declared, and a typo creates rather than raises. The rectangle design decision is the most interesting content here and the easiest to read past, since it has no code. And the aliasing is chapter 10 unchanged, which is either reassuring or worth revisiting depending on how solid that chapter felt.
Connect it up
One page, from memory.
Draw it
Draw the class object with two instances hanging off it, labelling which is the factory and which are the products, and write the printed form of each. Then draw the book's figure 15.2 — a Rectangle with width, height and an embedded Point — and beside it write what box.corner.x means, as two steps. Finally list the two ways to represent a rectangle and one operation each makes easy.
Recap
Three pages, and you can make a type of your own.
| If you remember one thing | It is this |
|---|---|
| From the class statement | One class object, many instances. Printing tells you which you have. |
| From attributes | They come into being when assigned. Nothing is declared and nothing is checked. |
| From the double dot | box.corner.x is two steps, and the first one must find something. |
| From the rectangle | The attributes are a choice, decided by the operations you will need. |
| From arguments | An instance is a mutable object, so chapter 10's aliasing rules apply unchanged. |
The next lesson returns objects from functions, distinguishes mutable objects from immutable ones, copies instances properly with the copy module — including the difference between a shallow and a deep copy — and covers the AttributeError and the hasattr function for debugging.
Think Python, 2nd edition — Allen B. Downey §15.1-15.3, pp. 147-149 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.