15a Programmer-Defined Types, Attributes, and Rectangles

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

What this lesson covers

The lesson, slide by slide

1. Lesson 15a Programmer-Defined Types, Attributes, and Rectangles

Title

Python · Chapter 15 — Classes and objects

§15.1-15.3, pp. 147-149

2. By the end of this lesson you can

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

3. Before we start: three ways to hold a point

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.

4. The one idea behind this chapter: you can make a new type

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

The same two numbers. The difference is whether the parts have names.

Think Python, 2nd edition — Allen B. Downey §15.1-15.3, pp. 147-147

5. Defining a class

Section

Section 1

6. A header, and a docstring

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."""
PartWhat it doesNote
the headernames the new classPoint
the bodya docstringexplaining what it is for
the resulta class objectwhich 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

7. Picture it: a class definition creates an object

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

The class object is like a factory for creating objects.

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.

8. Worked example: the class object and an instance

Worked example

Printing each shows what kind of thing it is.

>>> Point
<class '__main__.Point'>
>>> blank = Point()
>>> blank
<__main__.Point object at 0xb7e9d3ac>
ExpressionWhat it isNote
Pointthe class objectprinted as <class ...>
Point()calling itcreates an instance
blankone instanceprinted 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

The whole run at once: each drop is one line of the program.

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.

9. Predict: what does calling the class produce?

Prediction

The class is called like a function.

class Point:
    """Represents a point in 2-D space."""

blank = Point()
print(type(blank))
StepWhat happensResult
Point()instantiationa new object
blankan instance of Point
type(blank)the class it belongs to<class '__main__.Point'>

Predict first

What does this print?

  • <class '__main__.Point'>
  • <__main__.Point object at 0x...>
  • Point
  • A TypeError, since the class defines no functions

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.

10. Worked example: instantiation, and the vocabulary

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
TermWhat it meansNote
instantiationcreating a new objectcalling the class
instancethe object createdof that class
objectthe same thingthe 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

One class, many instances. Calling the class is what makes each one.

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.

11. Trap: assigning to the class instead of an instance

Trap

The 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.

The fix

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.

12. Discriminate: the class or an instance?

Discrimination

One is a factory and the others are its products.

Sort into buckets

For each, which are you holding?

the class object
Point; the thing printed as <class '__main__.Point'>; what type(blank) returns
an instance
Point(); blank, after blank = Point(); the thing printed as <__main__.Point object at 0x...>
cls
Each is the single class object — the factory. Its printed form begins with the word class, and type of any instance returns it.
ins
Each is something the class produced. An instance prints as an object with an address, and each call to the class makes a new one.

13. Complete it: define a class

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.

14. Explain it yourself: why is a class an object?

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.

15. Attributes

Section

Section 2

16. Named parts, assigned with a dot

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
StatementWhat it doesNote
blank.x = 3.0creates the attributeand gives it a value
blank.yreads it4.0
x = blank.xcopies the valueinto 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

17. Picture it: figure 15.1, the object diagram

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

The book's figure 15.1. The variable blank refers to a Point object containing two attributes.

Each attribute refers to a floating-point number — the same reference picture as chapter 10, with names instead of positions.

18. Worked example: reading an attribute in an expression

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
UsageWhat happensResult
blank.xgo to the object and get x3.0
in a format expressionan ordinary valueno special handling
in arithmeticlikewise5.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 whole run at once: each drop is one line of the program.

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.

19. Predict: is there a conflict?

Prediction

A variable and an attribute share a name.

blank.x = 3.0
x = 99
print(blank.x, x)
NameWhere it livesValue
blank.xan attribute of the object3.0
xa variable99
the twoin different placesno conflict

Predict first

What does this print?

  • 3.0 99
  • 99 99
  • 3.0 3.0
  • An error about a name conflict

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.

20. Worked example: passing an instance to a function

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)
PartWhat happensNote
print_point(blank)passes a referenceas always
pan alias for blankone object, two names
p.xthe same attributeas 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

Inside the function, p is an alias for blank — chapter 10's picture, with an instance in the box.

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.

21. Trap: expecting an attribute to exist before it is assigned

Trap

The 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.

The fix

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.

22. Complete it: read two attributes

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.

23. Sort: what does the dot select?

Sorting

The same syntax, three different things behind it.

Sort into buckets

For each expression, what is the dot selecting?

something from a module
math.pi; string.whitespace; os.path
an attribute of an object
blank.x; box.corner; p.y
mod
Each selects a name defined inside an imported module — the syntax the book says attribute access is similar to.
att
Each selects a named element of an instance. The syntax is the same and the thing on the left is an object rather than a module.

24. Explain it: why is a Point better than a tuple?

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.

25. Choosing the attributes

Section

Section 3

26. Sometimes it is obvious, and sometimes it is a decision

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

27. Picture it: two representations of one rectangle

Picture it

The same shape, described two ways.

Figure (svg): Two columns comparing corner-plus-size with two-corners as rectangle attributes

Neither is better in the abstract, which is exactly what makes it a design decision.

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.

28. Worked example: the Rectangle class

Worked example

A docstring that lists the attributes, since nothing else does.

class Rectangle:
    """Represents a rectangle.

    attributes: width, height, corner.
    """
AttributeWhat it holdsNote
width, heightnumbersthe size
cornera Point objectthe lower-left corner
the docstringthe only record of thisnothing 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

The whole run at once: each drop is one line of the program.

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.

29. Predict: what does the double dot do?

Prediction

Two attribute selections in one expression.

box.corner = Point()
box.corner.x = 5.0
print(box.corner.x)
PartWhat it selectsNote
box.cornerthe embedded Pointstep one
.xan attribute of that Pointstep two
the result5.0

Predict first

What does box.corner.x mean?

  • Go to box, select corner, then go to that object and select x
  • Select an attribute of box named 'corner.x'
  • Select x from the Rectangle class
  • It is a syntax error — only one dot is allowed

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.

30. Worked example: an embedded object

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
AttributeWhat it holdsNote
box.widtha numberdirectly on the rectangle
box.cornera Point objectembedded
box.corner.xtwo dotsinto 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

The book's figure 15.2. The Point is drawn inside because it is an attribute of the Rectangle.

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.

31. Trap: forgetting to create the embedded object

Trap

The 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.

The fix

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.

32. Compare: two ways to represent a rectangle

Comparison

Fill the blanks. Each makes different operations easy.

Comparison matrix

Questioncorner, width, heighttwo opposing corners
How many objects?one Point and two numberstwo Points
Changing the widthassign to one attributemove one corner, computing the new position
Finding the widthread the attributesubtract the two x coordinates
Which does the book implement?this one, just as an examplenot 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.

33. Complete it: build the embedded point

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.

34. Think it through: how would you choose between the two?

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.

35. Objects as mutable arguments

Section

Section 4

36. An instance passed to a function is an alias

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
PartWhat happensNote
the callpasses a referencenot a copy
p.x = ...modifies the objectthrough the parameter
blank.xsees the changeone 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

37. Picture it: the same aliasing picture as chapter 10

Picture it

Two frames, one object.

Figure (svg): A state diagram showing a function parameter and the caller's variable referring to one Point

One box, two arrows. Chapter 10's figure 10.5, with an instance in place of a list.

Nothing about defining a class changed the rules. An instance is a mutable object, so all of chapter 10's warnings apply unchanged.

38. Worked example: modify or return, again

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
FunctionIts contractNote
move_rightmodifies, returns Nonethe caller's point changes
moved_rightbuilds a new Pointthe caller's is untouched
the call sitesdiffer visiblyone 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

Lesson 10c's two contracts, now for a programmer-defined type.

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.

39. Predict: does the caller see the change?

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)
StepWhat happensResult
the callp is an alias for blankone object
p.x = ...modifies the objectnot the name
blank.xthe same object5.0

Predict first

What does this print?

  • 5.0
  • 3.0
  • 2.0
  • None

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.

40. Worked example: an instance is not special

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
SituationWhat happensNote
q = pcopies the referenceone object
q.x = 99.0modifies itp.x sees the change
two equal Pointsstill 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

The whole run at once: each drop is one line of the program.

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.

41. Trap: assuming a returned object is a copy

Trap

The 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.

The fix

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.

42. Sort: does the caller's Point change?

Sorting

Ask whether the object or the name is being changed.

Sort into buckets

For each function body, does the caller see a difference?

the caller sees it
p.x = p.x + 1; p.y = 0.0; q = p; q.x = 9.0
the caller is unaffected
p = Point(); return p.x + 1; p = some_other_point
yes
Each modifies the object the parameter refers to — including the one that goes through a second local name, since that name is another alias for the same object.
no
Two repoint the local parameter, which the caller's arrow cannot see; the third only reads an attribute and returns a number.

43. Complete it: build a genuinely new Point

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.

44. Explain it yourself: why does chapter 10 still apply?

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.

45. What this style cannot yet do

Section

Section 5

46. A minimal class, and its gaps

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

47. Picture it: four gaps, and where each is filled

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

The minimal style is a starting point, not the finished technique.

Downey introduces classes this way deliberately: with nothing hidden, it is clear that an object is just a thing with named parts.

48. Worked example: the typo that creates an attribute

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'
StatementWhat happensNote
blank.Y = 4.0creates an attribute named Ylegal
blank.yno such attributeAttributeError
the docstringsays nothing about itand 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.

49. Predict: what happens on a fresh instance?

Prediction

Nothing has been assigned yet.

blank = Point()
print(blank.x)
StepWhat happensResult
Point()an instance with no attributesthe class defines none
blank.xnothing to findAttributeError
the docstringdocumentation onlycreates nothing

Predict first

What happens?

  • An AttributeError — nothing has assigned x
  • It prints 0.0, the default for a number
  • It prints None
  • It prints an empty line

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.

50. Worked example: what printing an instance gives you

Worked example

The address is not what you wanted.

>>> blank
<__main__.Point object at 0xb7e9d3ac>
>>> print_point(blank)
(3.0, 4.0)
ApproachWhat you seeNote
printing directlyclass and addressno contents
a helper functionthe contentswritten by hand
the eventual fixa __str__ methodtwo 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

The whole run at once: each drop is one line of the program.

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.

51. Trap: expecting a class to declare its attributes

Trap

The 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.

The fix

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.

52. Error analysis: four things that go wrong with a minimal class

Error analysis

Mark each and say what happens.

Annotate

  • Line 1 raises AttributeError: nothing has assigned x, and the class declares no attributes.
  • Line 2 is legal and creates an attribute named Y. If y was intended, the failure appears at the later read, far from the typo.
  • Line 3 is legal and unhelpful: it shows the class and the memory address rather than the coordinates. A helper function is the workaround at this stage.
  • Line 4 sets an attribute on the class object rather than an instance, so every Point appears to have it. Legal, and almost never what was meant.
  • So one raises and three are legal and misleading — which is characteristic of this minimal style, where nothing is declared and nothing is checked.
  • vars(obj) is the diagnostic for the first two: it lists what the object actually has, which usually makes the mistake obvious.

Three of the four are fixed by the next chapter's __init__ and __str__ methods.

53. Compare: a class now, and a class two chapters from now

Comparison

Fill the blanks. Each gap has a specific fix.

Comparison matrix

QuestionNowLater
Who assigns the attributes?code outside the classan __init__ method
What does printing show?the class and a memory addresswhatever __str__ returns
How are two instances compared?by identityby __eq__, if you define one
Where does behaviour live?in separate functions like print_pointin methods inside the class

The bottom row is the biggest change, and it is what object-oriented actually refers to.

54. Where naming the parts matters

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.

55. Compare: a tuple and a programmer-defined type

Comparison

Fill the blanks. Both hold two numbers.

Comparison matrix

Question(3.0, 4.0)a Point
How are the parts identified?by positionby name
Mutable?noyes — attributes can be assigned
Can it be a dictionary key?yesyes, but compared by identity
Is the type distinguishable?no — any pair looks the sameyes — 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.

56. The procedure: defining and using a simple class

Pattern

Six steps, in the minimal style this chapter uses.

  1. Write the class statement with a docstring saying what it represents.
  2. List the intended attributes in the docstring, since nothing else records them.
  3. Create an instance by calling the class like a function.
  4. Assign each attribute with dot notation — no attribute exists until you do.
  5. Read attributes with the same syntax, anywhere an expression is allowed.
  6. Write functions that take instances as arguments, remembering the parameter is an alias.

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

57. Check yourself 1 of 3: class and instance

Check

Two things, both printed.

Check your understanding

Which of these is printed as <class '__main__.Point'>?

  • A. Point (correct)
  • B. Point()
  • C. blank, after blank = Point()
  • D. blank.x

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.

Why B tempts people
Calling the class creates an instance, which prints as an object with an address.
Why C tempts people
Also an instance, for the same reason.
Why D tempts people
An attribute value — here a float, which prints as a number.

58. Check yourself 2 of 3: the double dot

Check

One expression, two steps.

box.corner.x = 0.0
PartWhat it doesNote
box.cornerselect corner from boxstep one
.xselect x from that objectstep two
the requirementcorner must already exist

Check your understanding

What must be true for this line to work?

  • A. box.corner must already refer to an object (correct)
  • B. The Rectangle class must declare a corner attribute
  • C. Point must define an x attribute
  • D. Nothing — both attributes are created by this line

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.

Why B tempts people
Classes declare nothing at this stage. The docstring is documentation only.
Why C tempts people
Point defines nothing either. Its x attribute is created by this very assignment.
Why D tempts people
Only the last attribute in a chain is created by an assignment. Every earlier step is a read.

59. Check yourself 3 of 3: instances as arguments

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?

  • A. Their Point's y is now 0.0 (correct)
  • B. Their Point is unchanged
  • C. A new Point is returned
  • D. An AttributeError, since functions cannot assign attributes

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.

Why B tempts people
This would be true if the function assigned to the parameter itself, which repoints a local name.
Why C tempts people
Nothing is returned unless the function says so. A modifying function returns None.
Why D tempts people
Assigning an attribute through any reference is perfectly legal, which is exactly what makes instances mutable.

60. Where this shows up outside this course

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.

61. Confidence wager: commit before you check

Commit first

Answer, then rate your confidence.

Predict first

You write blank = Point() and then print(blank.x). What happens?

  • An AttributeError — nothing has assigned x yet
  • It prints 0.0, the default value
  • It prints None
  • A NameError, since x is not defined

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.

62. Explain it to someone else

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.

63. Exit ticket

Exit ticket

One honest answer. It decides what the next lesson opens with.

Predict first

Which of these is still least solid for you?

  • The class statement, and the difference between a class and an instance
  • Attributes, dot notation, and the object diagram
  • Choosing which attributes a class should have
  • Instances as arguments, and the aliasing that follows

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.

64. Synthesis: draw the map of this lesson

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.

65. What you can do now

Recap

Three pages, and you can make a type of your own.

If you remember one thingIt is this
From the class statementOne class object, many instances. Printing tells you which you have.
From attributesThey come into being when assigned. Nothing is declared and nothing is checked.
From the double dotbox.corner.x is two steps, and the first one must find something.
From the rectangleThe attributes are a choice, decided by the operations you will need.
From argumentsAn 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

Sources

  1. Think Python, 2nd edition — Allen B. Downey — Allen B. Downey, Think Python: How to Think Like a Computer Scientist, 2nd edition (Green Tea Press, 2015), §15.1-15.3, pp. 147-149
  2. Python documentation — Classes
  3. Python documentation — Data Structures

Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108