COMP 202 Foundations of Programming • McGill University, Montreal

Revision sheet: Classes and objects (COMP 202)

This sheet is not a summary of the last chapter of COMP 202: you already have the slides. It answers one question only, what makes students lose marks on classes and objects, on the paper final and in the autograded final assignment, and which precise gesture avoids each loss.

The chapter looks like syntax and is really about PLACE. Every trap below is the same question asked in a different costume: does this name live on the object, on the class, or in the local frame of the call? Answer it and self, class attributes, __eq__ and super stop being mysterious. The final assignment is usually built on this chapter, so an error here is paid on every method of the class you hand in.

Mark this sheet as read or add it to your favourites: a free account, no password, keeps your read sheets and favourites from one visit to the next and tells you which chapter to tackle next. Create your space, an email is enough.

The thread of the chapter

An object bundles a STATE with the operations allowed on it, and self is nothing more than the object to the left of the dot. Almost every mark lost on this chapter comes from mistaking WHERE something lives: on the object, on the class, or in a local name that dies with the call.

This chapter is part of COMP 202, Foundations of Programming (McGill)

The essentials

self is the object to the left of the dot, and a name has ONE place to live

  • • ana.average() is executed as Student.average(ana): Python passes the object as the first argument, so every method header starts with self, even a method that seems to need nothing.
  • • __init__ does not create the object, Python does. It INITIALISES it, and it is the one place where the whole state is declared: every attribute the class will ever read is assigned there, with self.
  • • Reading self.x looks on the object first, then on the class. Assigning self.x = v ALWAYS creates or replaces x on the object, never on the class.
  • • An attribute written in the class body, outside every method, exists once and is shared by every object. It is the right place for a constant or a counter, and the wrong place for anything mutable.
  • • A bare name inside a method, price, is a local variable or a global: it never reaches the attribute. The attribute is self.price, and forgetting the self. is a NameError, not a typo.
class Studentcount = 0average(self)object ananame = 'Ana'count = 1object benname = 'Ben'ana.count = ana.count + 1 CREATED count on ana; Student.count is still 0reading falls back to the class, assigning never doesben.count reads 0: not on ben, so Python looks in the class
Two objects, one class. Assigning ana.count created a count ON ana that hides the class attribute; ben has none, so ben.count still reads the 0 of the class.

On a trace question, write next to each attribute line WHERE it acts: on the object, on the class. Markers give the method mark for that annotation, and it is what catches the hidden counter of the second trap below.

Four calls Python makes for you, and what happens if the method is missing

  • • print(obj) and str(obj) call obj.__str__(), which must RETURN a string. Missing: the class name and a memory address.
  • • obj1 == obj2 calls obj1.__eq__(obj2). Missing: identity, so two objects built separately are never equal, and in, index and remove follow the same rule.
  • • sorted(objects) calls __lt__. Missing: TypeError, and the repair is a key, sorted(objects, key=lambda t: t.price), which also says on what the order is based.
  • • obj1 is obj2 asks whether the two NAMES refer to the same object in memory. No method is called, no class can change it, and it is the only honest answer to the question is this the same object.

The convention for a method that CHANGES the object is to return None, like list.sort. A mutating method that returns self reads as a pure one, and the marker who sees new = old.add(x) will ask what old is now.

The class is a promise, and every path into the state goes through it

  • • An invariant is a property that holds after every method: a balance is never negative, a mark is between 0 and 100. __init__ establishes it, every method preserves it, and a method that would break it RAISES ValueError before touching anything.
  • • Python has no private attributes. The leading underscore, self._balance, is a convention that tells the reader do not touch this from outside; it stops nobody, and it is the expected answer in COMP 202.
  • • A derived value is a METHOD, not an attribute computed in __init__: an average stored at construction is the average of an empty list for ever.
  • • Composition is HAS A, a Course holds a list of Student objects; inheritance is IS A, a SavingsAccount is an Account. The test: every method of the parent must still make sense on the child.

A refused operation leaves the object exactly as it was: the check comes BEFORE the assignment. That order is worth a mark on its own in the final assignment, because the autograder tests the state after an invalid call.

The rules in table form

Each row reads from left to right: the assumptions, then the result. A red cell is not an answer, it is the finding that the form settles nothing and the instruction to rewrite it. Every case is followed by a worked example.

Where Python looks when a name is read or written inside a method

Read a line as: this expression, written inside a method of Student, looks in these places and finds this. The red lines are lookups students believe in and that do not exist.

WrittenLooked up onFinds
self.count the object, then the class the object's own if any, else the class's

Example: With count = 0 in the class body and no assignment on ana, ana.count reads 0.

self.count = v the object only creates or replaces count ON the object

Example: self.count = self.count + 1 on a fresh object gives ana.count = 1 and leaves Student.count at 0.

Student.count = v the class only the shared value is changed

Example: Two objects built with Student.count = Student.count + 1 in __init__ leave Student.count at 2.

count, bare the local frame, then globals a local or global variable, never the attribute

Example: def total(self): return count * 2 raises NameError even though the class has count = 0; the line to write is self.count * 2.

count, bare the object the attribute lookup that does not exist

Example: A student writes marks.append(8) inside add_mark and gets NameError: with marks = [] nowhere in the frame, Python finds nothing, and 0 marks are stored.

What to do: Every attribute is reached through self, without exception: self.marks.append(8).

self.x, x never assigned in __init__ the object, then the class the attribute lookup that does not exist

Example: A Student whose marks are created in add_mark raises AttributeError in average() when 0 marks have been added: the attribute does not exist yet.

What to do: Declare the whole state in __init__: self.marks = [] there, so that every object starts complete.

One rule covers the whole table: reading falls back to the class, assigning never does, and a bare name never reaches the object.

The mistakes that cost marks

These are the errors I correct most often in session. Each one costs marks on a paper, even when the reasoning behind it is right.

1. A method header written without self

the method, and every autograder test that calls it: TypeError, average() takes 0 positional arguments but 1 was given

What not to write

“def average(): return sum(self.marks) / len(self.marks)”

What to write

“def average(self): return sum(self.marks) / len(self.marks)”, and the call stays ana.average() with nothing between the brackets.

Why: The call ana.average() looks as though it passes nothing, and it passes ana. The message says 1 was given because the object IS the argument, and the header declared no parameter to receive it.

2. Increasing a class counter through self

the whole counter question, and the trace that follows it

What not to write

“count = 0 in the class body, then in __init__: self.count = self.count + 1. After two students, Student.count is 2.”

What to write

“In __init__: Student.count = Student.count + 1. Reading self.count would fall back to the class, but ASSIGNING self.count creates a new attribute on the object and leaves the class attribute at 0.”

Why: Assignment never climbs to the class. Each object ends up with its own count = 1 and Student.count stays 0 for ever, so every student claims to be the first.

3. Putting a list in the class body

2 to 4 marks, and an autograder that fails every test after the first one

What not to write

“class Student: marks = [] then def __init__(self, name): self.name = name. Each student has an empty list to start with.”

What to write

“Each student needs its OWN list, so it is created in __init__: self.marks = []. A list in the class body is built once, when the class is read, and shared by every object.”

object anamarksobject benmarks8marks = [] in the class bodyONE list, built when the class was readsharedobject anamarksobject benmarks8self.marks = [] in __init__one list PER object, built at each callempty
Left, marks = [] in the class body: both objects point at ONE list, so the 8 given to ana is read through ben. Right, self.marks = [] in __init__: each object owns its list.

Why: It is the mutable default argument again, one floor up: one list created at definition time and reached by everything that comes later. A mark appended for Ana appears in Ben's marks.

4. A __str__ that prints instead of returning

the question, and a crash: TypeError, __str__ returned non-string (type NoneType)

What not to write

“def __str__(self): print(self.name + ', ' + str(self.average()))”

What to write

“def __str__(self): return self.name + ', ' + str(self.average()). print(ana) prints WHAT __str__ RETURNS.”

Why: It is print against return, in the one place where the language checks it. The text appears once, at the wrong moment, then print receives None and refuses it.

5. Comparing two objects with == and reading the answer as a comparison of values

the whole question on a trace, and a silent bug in any in, index or remove on a list of objects

What not to write

“Ticket('A1', 40) == Ticket('A1', 40) is True, they have the same seat and the same price.”

What to write

“Without __eq__, == compares IDENTITY, so two objects built separately are never equal: the answer is False. With def __eq__(self, other): return self.seat == other.seat, it is True.”

acbTicketseat = 'A1'price = 40Ticketseat = 'A1'price = 40a is c: True, one object, two namesa is b: False, two objectsa == b: False with no __eq__a == b: True once __eq__ compares seatsc.discount(50) changes the objectthat a also names; b is untouched
a and c name the SAME object, b names a second one with equal attributes: a is c is True, a is b is False, and a == b depends on whether __eq__ was written.

Why: Python cannot guess which attributes make two objects the same, so its default is the only safe one, same object in memory. a is b asks that question openly and no class can change its answer.

6. A child __init__ that forgets the parent's

every inherited method: deposit, withdraw and __str__ all raise AttributeError, 'SavingsAccount' object has no attribute 'balance'

What not to write

“class SavingsAccount(Account): def __init__(self, owner, balance, rate): self.rate = rate”

What to write

“def __init__(self, owner, balance, rate): super().__init__(owner, balance) FIRST, then self.rate = rate.”

Why: Redefining __init__ REPLACES the parent's, it does not extend it. The owner and the balance are only created by Account.__init__, and nobody called it.

7. Calling a method without its brackets

the question: TypeError, '>' not supported between instances of 'method' and 'int', or a print that shows bound method Student.average of ...

What not to write

“if ana.average > 60: print('pass')”

What to write

“if ana.average() > 60: print('pass'). Without the brackets, ana.average is the method itself, not its value.”

Why: An attribute is read without brackets, a method is CALLED with them. The two look alike after a dot, and the error only appears when the result is used.

8. Reaching into an object from outside the class

2 marks in the final assignment, and an invariant that no longer holds anywhere in the program

What not to write

“course.students[0].marks.append(130), it is faster than going through add_mark.”

What to write

“course.students[0].add_mark(130), which raises ValueError: 130 is not a mark. The list is the inside of Student, and only Student may write to it.”

Why: The whole value of a class is the promise it keeps, and a promise kept only when callers are polite is not one. Nothing in Python stops the line, so it is the reader, and the marker, who enforces it.

9. Inheriting because two classes share a word

the design question, 3 to 5 marks, and a class that inherits methods it must never expose

What not to write

“class Course(Student): both have a name and an average, so Course inherits from Student.”

What to write

“A Course is not a Student: add_mark makes no sense on a course. A Course HAS students, so class Course: with self.students = [] in __init__, and Course.average() asks each student for its own.”

Why: The test is IS A, applied to every method of the parent: if one of them is nonsense on the child, the relation is composition. Sharing an attribute name is not sharing a kind.

10. Computing a derived value in __init__ and storing it

1 to 2 marks, and an autograder that reads 0 for a student with marks 80 and 90

What not to write

“In __init__: self.marks = [] and self.average = 0. Then add_mark appends to the list.”

What to write

“def average(self): return None if self.marks == [] else sum(self.marks) / len(self.marks). It is derived from the state, so it is a method that reads the state at call time.”

Why: A stored derived value is right at one instant and wrong after the first change unless every mutating method updates it. Store what is independent, compute what follows from it.

Which method to choose

Where does it go? Decide by what the statement says about the value

Read the description of the class in the assignment, and for each piece of information ask who it belongs to and whether it can change

Coursetitlestudents: listStudentname, marksStudentname, marksHAS A: compositiona Course holds Students in a listeach class answers only about itselfAccountowner, balancedeposit(), withdraw()SavingsAccount(Account)rateadd_interest()IS A: inheritancea SavingsAccount IS AN Account, every method still makes sensechild __init__ calls super().__init__(owner, balance) first
Left, HAS A: a Course keeps its Student objects in a list and asks them for their averages. Right, IS A: a SavingsAccount is an Account with one attribute and one method more, and its __init__ calls the parent's first.
  • If the value belongs to ONE object and differs from one to the next: a name, a balance, a list of marks → instance attribute, assigned in __init__ with self, mutable ones created fresh there

    Example: self.marks = [] in __init__, never marks = [] in the class body

  • If the value is the same for every object of the kind, or counts them: a passing grade of 60, a number of tickets issued → class attribute in the class body, updated through the CLASS name

    Example: issued = 0 in the body, Ticket.issued = Ticket.issued + 1 in __init__: 2 after two tickets

  • If the value FOLLOWS from the state: an average, a total, an age from a birth year → a method that computes it at call time, with a guard for the empty case

    Example: def average(self): returns None for 0 marks, else sum divided by len, 85 for marks 80 and 90

  • If the statement says an X HAS a Y, or a list of Y: a course has students, an order has lines → composition: an attribute holding the object or the list of objects, and each class answers only about itself

    Example: self.students = [] in Course.__init__; Course.average() calls s.average() on each of its 3 students

  • If the statement says an X IS A Y and every method of Y still makes sense on X → inheritance: class X(Y), __init__ starting with super().__init__(...), only the methods that change are redefined

    Example: class SavingsAccount(Account) adds rate and add_interest, 1000 dollars at 2 percent become 1020

    if one method of Y is nonsense on X, it is not IS A: go back to composition

  • If the statement says the objects are printed, compared, searched in a list, or sorted → __str__ that RETURNS a string, __eq__ that names the attributes of equality, sorted with a key

    Example: sorted(tickets, key=lambda t: t.price) puts 30 before 40; without a key, TypeError

If no branch applies, the information is probably a parameter of a method, not an attribute: a discount percentage is passed to discount(pct), it is not stored on the ticket. Properties and private name mangling are outside COMP 202.

How the answer is expected to be written

A marker ticks steps. Here they are in order, with the concluding sentence expected word for word.

Writing a class for the final assignment, in the order the autograder tests it

When to use it: The assignment describes a kind of thing with its data and its operations and asks for a class, usually with a validation rule and a printed form

  1. 1 Write __init__ first, with the WHOLE state assigned through self, mutable attributes created fresh, and the invariant checked on the parameters: a negative starting balance raises ValueError before anything is stored.
  2. 2 Write __str__ next, returning one string and printing nothing; test it at once with print on one object, because an address in the output means the method was not found.
  3. 3 Write each operation as a method with self first. Decide whether it CHANGES the object or RETURNS a new value, say so in the docstring, and return None when it changes.
  4. 4 Put the validation at the top of every mutating method, with a raise, so that a refused call leaves the object untouched.
  5. 5 Write __eq__ only if the assignment compares or searches objects, and say in it which attributes make two objects equal.
  6. 6 Build two objects, mutate one, print both: if the second changed, a mutable attribute lives in the class body.

Concluding sentence

“def withdraw(self, amount): "Takes amount out of this account and returns None; raises ValueError if amount is not positive or exceeds the balance."”

The trap: Writing the methods first and __init__ last, so that attributes are created here and there in the methods that first need them, and every test that calls the methods in another order fails with AttributeError.

Marking: Typically 2 marks for a complete __init__, 1 for __str__, 2 to 3 per method including its validation, 1 for the docstrings

Tracing a program with classes on paper

When to use it: The exam gives a class of ten lines and a main program of five, and asks for the output

  1. 1 Count the constructor calls: each ClassName(...) is ONE new object. An assignment such as c = a creates no object, only a second name for an existing one.
  2. 2 Draw one box per object with its attributes, and one box for the class with its class attributes; put every name on the left with an arrow to its object.
  3. 3 For each method call, write the object to the left of the dot next to self, then run the body line by line, changing the box the arrow points at.
  4. 4 For each attribute line, note where it acts: self.x = v writes in the object's box, ClassName.x = v in the class box, and a read of self.x that finds nothing in the object box goes to the class box.
  5. 5 For print(obj), write what __str__ RETURNS; for obj1 == obj2, look for __eq__ and, if there is none, compare the arrows, not the boxes.

Concluding sentence

“b.discount(25): self is b, so b.price becomes 40 times 0.75, that is 30.00; a and c still point at the object whose price is 40.”

The trap: Reading c = a as a copy: the two names share one object, and a change made through c is printed through a.

Marking: Usually 1 mark per output line, and the method mark for the box diagram

Check before you hand in

Five minutes of checking recover more marks than one more problem started in a hurry.

The typical problem, taken apart

The output of a program with a class attribute, an alias and an __eq__

Give the exact output of the program below, and justify each printed value in one sentence, as on a COMP 202 final.

The class Ticket carries a class attribute issued, an instance method discount that changes the price, a __str__ and an __eq__ that compares seats only.

python
class Ticket:
    issued = 0

    def __init__(self, seat, price):
        self.seat = seat
        self.price = price
        Ticket.issued = Ticket.issued + 1

    def discount(self, pct):
        self.price = self.price * (1 - pct / 100)

    def __str__(self):
        return f'seat {self.seat}: {self.price:.2f}'

    def __eq__(self, other):
        return self.seat == other.seat

a = Ticket('A1', 40)
b = Ticket('A1', 40)
c = a
b.discount(25)
print(Ticket.issued, a.issued)
print(a == b, a is b, a is c)
print(b)
c.discount(50)
print(a)

Step 1

Count the objects: Ticket('A1', 40) is called twice, so there are exactly two Ticket objects. c = a creates none, it is a second name for the object that a names.

Why

Everything else in the trace depends on that count: the counter, the identity tests and the last line all follow from two objects and three names.

Step 2

Each __init__ runs Ticket.issued = Ticket.issued + 1, written with the CLASS name, so Ticket.issued goes from 0 to 1 to 2. a.issued finds no issued on the object and falls back to the class: 2. First line printed: 2 2.

Why

Had the line been self.issued = self.issued + 1, each object would own an issued equal to 1 and the class would still read 0: the line would print 0 1. Naming where the assignment acts earns the method mark.

Step 3

b.discount(25): self is b, so b.price becomes 40 times (1 minus 25 over 100), that is 40 times 0.75, so 30.0. The object named by a and c is untouched and still holds 40.

Why

The method changes the object to the left of the dot and returns None; nothing is assigned from the call, which is the convention for a mutating method.

Step 4

a == b calls a.__eq__(b), which compares the seats, both 'A1': True. a is b asks whether the two names refer to the same object: two constructor calls, so False. a is c: c was assigned a, so True. Second line printed: True False True.

Why

The equality sign answers what __eq__ defines, the price being ignored on purpose; is answers on the arrows of the diagram and no method can change it.

Step 5

print(b) calls b.__str__(), which returns seat A1: 30.00, the price being formatted with two decimals. Third line printed: seat A1: 30.00.

Why

print displays what __str__ RETURNS; a __str__ that printed would have shown the text and then crashed on the None.

Step 6

c.discount(50): self is c, which names the same object as a, so that object's price becomes 40 times 0.5, that is 20.0. print(a) reads that same object: seat A1: 20.00. Fourth line printed: seat A1: 20.00.

Why

This is the alias check of the trace: a change made through one name is printed through the other, because there is one object behind both.

Step 7

Check: two objects were created, so Ticket.issued is 2; a and c share one, b is alone; 40 times 0.75 is 30 and 40 times 0.5 is 20, not 30 times 0.5, since a was never discounted before.

Why

Rereading the diagram takes ten seconds and catches the two classic slips, 15.00 on the last line and 0 1 on the first.

The conclusion, written out

“The program prints four lines: 2 2, then True False True, then seat A1: 30.00, then seat A1: 20.00. There are two Ticket objects and three names; a and c name the same one.”

The classic mistake on this problem: Printing seat A1: 40.00 on the last line, on the grounds that only c was discounted: c and a are two names for one object, and discount changed it.

Learn by heart

  • • obj.method(x) is executed as ClassName.method(obj, x): self is the object to the left of the dot, and every method header starts with it.
  • • __init__ initialises an object Python has already created; the WHOLE state is assigned there, with self, mutable attributes built fresh.
  • • Reading self.x falls back to the class; assigning self.x always writes ON THE OBJECT. A class counter is updated through ClassName.x.
  • • An attribute in the class body exists once and is shared: constants and counters only, never a list or a dictionary.
  • • print(obj) calls __str__, which RETURNS a string; == calls __eq__, and without it compares identity; is always compares identity.
  • • A mutating method validates first, raises ValueError on a bad value, changes the state, returns None.
  • • HAS A is composition, an attribute holding objects; IS A is inheritance, class Child(Parent) with super().__init__(...) on the first line of the child __init__.

Frequently asked questions

What is self in a Python class?

self is the object the method was called on. When you write ana.average(), Python runs Student.average(ana), so inside the method self is ana and self.marks is Ana's list of marks. It is the first parameter of every method, supplied automatically by Python, and the word itself is only a convention, though one that every marker expects.

Why does print show something like Student object at 0x7f9c1d2b instead of my data?

Because the class has no __str__ method, so Python falls back on the class name and the memory address of the object. Write def __str__(self) that returns a string built from the attributes, for example the name followed by the average. It must return the string, not print it: print displays what __str__ returns, and a __str__ that prints crashes with a TypeError.

What is the difference between a class attribute and an instance attribute?

An instance attribute is written with self inside a method, usually __init__, and belongs to one object: each student has its own name and its own marks. A class attribute is written in the class body, outside every method, exists once and is shared by every object: a counter of objects created or a passing grade. Reading falls back from the object to the class, but assigning through self always creates the attribute on the object.

Why does == return False for two objects that have the same values?

Because without an __eq__ method, the equality sign compares identity: it asks whether the two names refer to the same object in memory, and two objects built by two calls to the constructor are two objects. Define def __eq__(self, other) and return the comparison of the attributes that make two objects equal, for example the seat number. The in operator on a list of objects uses the same rule.

When should I use inheritance rather than composition in COMP 202?

Use inheritance only when the sentence an X is a Y is true and every method of Y still makes sense on X: a savings account is an account, so deposit and withdraw apply to it. Use composition, an attribute that holds another object or a list of them, when X has a Y: a course has students. Sharing an attribute name such as name or average is never a reason to inherit.

What does super().__init__ do in a subclass?

It runs the parent's __init__ on the object being built, so that the attributes the parent declares, such as the owner and the balance of an account, exist on the child object too. Redefining __init__ in the child replaces the parent's version instead of extending it, so the call to super().__init__ with the parent's parameters must be the first line of the child's __init__, before the child adds its own attributes.

Practise it

Corrected exercises: Classes and objects, COMP 202

A method is proved on a paper, not on a sheet. The set for the same chapter takes each of these traps into a problem, with the solution written out step by step.

  • 10 corrected exercises
  • 100 points
  • 150 minutes
Do the exercises
Previous sheet Recursion, and when a loop is the better answer

© Ahmed Squalli Houssaini. Revision sheet published at www.letuteurscientifique.ca/en/fiches/comp202-classes-and-objects. Free for personal and classroom use; republishing it elsewhere requires written permission (legal notice).

See also

Stuck in COMP 202?

I tutor COMP 202 at McGill in English or in French, in person in Montreal or online. Get in touch for a first session before the final assignment is due.

Site by Studio Squalli