SchoolWhool
Quizzes and saved progress, freeSign in freeSign in

Python 6.1Written lesson12 minLesson 1 of 3 in Module 6

Classes and Objects

Bundle data and behaviour into objects, and read code that does.

Video in production.

Read the lesson: Classes and Objects

01Related values in separate variables, and why classes exist

So far, you have stored a product review as separate values. Here is the tokenise function from earlier, followed by one review in three pieces: the product, the kettle; a rating of five; and the text of the review.

def tokenise(review):
    text = review.strip().lower()
    for mark in ".,!?":
        text = text.replace(mark, "")
    return text.split()
product = "kettle"
rating = 5
text = "Great kettle! Boils fast, and it is quiet."
print(product, rating)
kettle 5

As long as the three values are separate, it is up to you to make sure the kettle's rating is never used with the headphones' text. Nothing in Python connects them.

As a dictionary

You could put the three values into one dictionary instead, with the keys product, rating and text:

review = {"product": "kettle", "rating": 5, "text": "Great kettle! Boils fast, and it is quiet."}
review["rating"]
5

That keeps them together, and review["rating"] gives 5. But a typo in a key, like rating without its "i", is a KeyError that you only find out about when that line runs:

review["ratng"]
KeyError: 'ratng'

And the functions that work on a review, like tokenise, are still defined somewhere else, separate from the data they work on.

So how do you keep a review's data and the code that uses it in one place? In Python, you do that with a class.

02class and `__init__`

A classKey termA definition of a new kind of object: class Review:. Calling it, Review(...), creates an instance. is a definition of a new kind of object. You write the keyword class, then the name, Review, with a capital "R", which is how class names are written, and then a colon. Inside it, indented, you define functions. A function defined inside a class is called a methodKey termA function defined inside a class, called on an object with a dot: review.tokens()..

class Review:
    def __init__(self, product, rating, text):
        self.product = product
        self.rating = rating
        self.text = text

    def tokens(self):
        return tokenise(self.text)

    def is_positive(self):
        return self.rating >= 4

The two methods at the bottom, tokens and is_positive, are the subject of the next section. Start with the first method, which has a special name.

The __init__ method

It is init, with two underscores before it and two after it: __init__. People say "dunder init", which is short for double underscore. Python calls __init__ every time a new review is created, so that is where each review gets its data.

Its first parameter is self, and after that come product, rating and text. Its body sets self.product, self.rating and self.text from those three arguments.

Creating a review

Running the class statement defines the class, but it does not create any reviews yet. To create one, you call the class like a function: Review("kettle", 5, ...) with the kettle review's text.

Python makes a new, empty object, then calls __init__ with that object as self, and __init__ stores the three values on it. That new object is called an instanceKey termAn object created from a class: review = Review("kettle", 5, "…"). of the class, and the values stored on it are its attributes.

review = Review("kettle", 5, "Great kettle! Boils fast, and it is quiet.")
type(review)
__main__.Review

So the type of review is the class Review. You read an attribute with a dot:

review.rating
5

A second review

Every call to the class makes a separate object. So this gives you a second review, for the headphones:

other = Review("headphones", 2, "Not great. The left side is quiet, and it is not easy to fix?")
other.product
'headphones'

Your first review still says kettle, because each instance has its own attributes:

review.product
'kettle'

03Methods and `self`

Now look at the functions that work on a review, which the class already defines. The method tokens returns tokenise(self.text), and is_positive returns whether self.rating is at least four. Both take self as their first parameter, and self is the review they were called on.

So review.tokens() gives the eight words of the kettle review:

review.tokens()
['great', 'kettle', 'boils', 'fast', 'and', 'it', 'is', 'quiet']
Predict first

And review.is_positive() gives True. So what do you think other.is_positive() gives, for the two-star headphones? Think it through before reading on.

I have a guess. Show meHide the answer
review.is_positive(), other.is_positive()
(True, False)

False, because two is less than four. It is the same method, and the only thing that changed is the object before the dot.

What self is

When you write review.is_positive(), with parentheses, Python finds is_positive in the class and calls it with review as self. So Review.is_positive(review), with review inside the parentheses, is exactly the same call, and it gives True as well:

Review.is_positive(review)
True

So far

Pause there for a moment and collect the ideas:

  • A class defines a new kind of object.
  • __init__ gives each new object its attributes.
  • A method is a function in the class that receives the object it is called on, as self.

04Instance attributes, vars and aliasing

So what is actually inside a review? vars(review) shows you its attributes:

vars(review)
{'product': 'kettle',
 'rating': 5,
 'text': 'Great kettle! Boils fast, and it is quiet.'}

It returns a dictionary, with product set to 'kettle', rating set to 5, and the text. So an object's attributes are stored as names and values, the same way a dictionary stores its keys and values.

The default display

If you display the review itself, you get __main__.Review, the word at, and then a long number:

review
<__main__.Review at 0x74ea4d31ae40>

That number is where the object is in memory. It is different every time the notebook runs, so yours will not match this one.

Changing an attribute

You can also change an object after it is created. review.rating = 4 changes that attribute, and now review.rating gives 4:

review.rating = 4
review.rating
4

Two names, one object

Predict first

The rule you learned with variables still holds here: a name refers to an object. Say you write same = review, and then same.rating = 1. What do you think review.rating is now? Think it through before reading on.

I have a guess. Show meHide the answer
same = review
same.rating = 1
review.rating
1

One. same = review did not copy anything. It bound a second name to the same object, the same way backup = cart did with lists. So same is review gives True, and a change through either name is a change to that one review:

same is review
True

05Special methods

That default display does not show much. Methods whose names start and end with two underscores are special methods, and Python calls them itself, at particular moments. One of them is __repr__, which Python calls whenever it needs to display an object.

__repr__

So add a __repr__ that returns the word Review, with the product and the rating in parentheses. Here is the class again with that method added, and the kettle review created again from it:

class Review:
    def __init__(self, product, rating, text):
        self.product = product
        self.rating = rating
        self.text = text

    def __repr__(self):
        return f"Review({self.product!r}, {self.rating})"

    def tokens(self):
        return tokenise(self.text)

    def is_positive(self):
        return self.rating >= 4
review = Review("kettle", 5, "Great kettle! Boils fast, and it is quiet.")
review
Review('kettle', 5)

Now displaying the review gives Review('kettle', 5), and a list of two reviews shows both of them the same way:

[review, Review("headphones", 2, "Not great. The left side is quiet, and it is not easy to fix?")]
[Review('kettle', 5), Review('headphones', 2)]

__eq__

Another special method is __eq__, which Python calls for ==. The class does not define one, and without it, == checks whether the two sides are the same object, the way is does. So two reviews with the same product, rating and text give False, because they are two separate objects:

Review("kettle", 5, "Great kettle! Boils fast, and it is quiet.") == Review("kettle", 5, "Great kettle! Boils fast, and it is quiet.")
False

Python does have a way to give a class an __eq__ that compares the data, without writing it yourself, and you will meet it soon.

__add__

The operators you already know work the same way. + calls __add__ on the value on its left. So (3).__add__(4) gives 7, and "a".__add__("b") gives 'ab':

(3).__add__(4), "a".__add__("b")
(7, 'ab')

That is how the same + sign can add numbers and join strings: each type has an __add__ of its own.

06Built in types are classes

That shows something that is true of all of Python. Every value is an object, and every object is an instance of some class.

The type of the string "hi" is str:

type("hi")
str

So str is a class, and upper is one of its methods. "hi".upper() gives 'HI'. And str.upper("hi") gives the same thing, because the string before the dot is passed in as self:

"hi".upper(), str.upper("hi")
('HI', 'HI')

Lists work the same way, so list.append(cart, "bread") does exactly what cart.append("bread") does:

cart = ["milk"]
list.append(cart, "bread")
cart
['milk', 'bread']

So every method call you have made since you started with Python has been this kind of call, from cart.append to review.strip. And when you use libraries later in the course, like pandas, the head method of one of its tables is a method of a class too.

07Inheritance

You can also define a class from another class, and then it starts with everything the other class has. That is called inheritanceKey termDefining a class from another, so that it starts with everything the other has: class VerifiedReview(Review):..

An exception type of your own

When you worked with exceptions, you raised a ValueError for a bad value. So make an exception type of your own, for a bad rating. class RatingError(ValueError):, with a body of pass, makes a new exception type, which is a kind of ValueError:

class RatingError(ValueError):
    pass

def check_rating(rating):
    if not 1 <= rating <= 5:
        raise RatingError(f"rating must be from 1 to 5, got {rating}")
    return rating

So check_rating(7) raises a RatingError:

check_rating(7)
RatingError: rating must be from 1 to 5, got 7

Caught as a ValueError

Because a RatingError is a ValueError, code that catches ValueError catches it too:

try:
    check_rating(0)
except ValueError as error:
    print("caught:", error)
caught: rating must be from 1 to 5, got 0

And issubclass(RatingError, ValueError) gives True:

issubclass(RatingError, ValueError)
True

So RatingError is what is called a subclassKey termA class defined from a base class: RatingError is a subclass of ValueError. of ValueError, and ValueError is its base class.

A subclass of Review

Here is a subclass of your own Review. A verified review also has an order ID, and it counts three stars as positive.

class VerifiedReview(Review):
    def __init__(self, product, rating, text, order_id):
        super().__init__(product, rating, text)
        self.order_id = order_id

    def is_positive(self):
        return self.rating >= 3

Its __init__ calls super().__init__ with the product, the rating and the text, which runs Review's own __init__ on it. Then it sets self.order_id. And it defines its own is_positive, which checks for at least three stars.

So a verified toaster review, with three stars and the order ID "A1042", has that order ID, and is_positive() gives True. tokens() gives toasts and evenly, and that method comes from Review, because VerifiedReview never defined tokens:

toaster = VerifiedReview("toaster", 3, "Toasts evenly.", "A1042")
toaster.order_id, toaster.is_positive(), toaster.tokens()
('A1042', True, ['toasts', 'evenly'])

isinstance(toaster, Review) gives True, because a verified review is still a review:

isinstance(toaster, Review)
True

But a plain Review with the same three stars gives False, because it uses Review's own is_positive:

Review("toaster", 3, "Toasts evenly.").is_positive()
False

08Common class errors: missing self, attribute typos and missing brackets

There are three mistakes that come with classes.

Leaving out self

The first is leaving self out of a method. Here is a class called Bad, whose method hello has no parameters at all:

class Bad:
    def hello():
        return "hi"
Bad().hello()
TypeError: Bad.hello() takes 0 positional arguments but 1 was given

You never passed an argument, but Python did, because it always passes the object before the dot as the first argument.

A typo in an attribute name

The second is a typo in an attribute's name. If you read one, like review.ratng without the "i", you get an AttributeError:

review.ratng
AttributeError: 'Review' object has no attribute 'ratng'
Predict first

But what happens if you set it instead, with review.ratng = 3? And what is review.rating afterwards? Think it through before reading on.

I have a guess. Show meHide the answer
review.ratng = 3
review.rating, review.ratng
(5, 3)

There is no error at all, and review.rating is still 5. Setting an attribute that does not exist creates it. So the review now has a new attribute called ratng, holding 3, and there is no error to tell you. That is one reason to set every attribute in __init__.

The type checker mypy does catch it, though. Run on a file containing the same review.ratng = 3 line, it reports that Review has no attribute ratng, and suggests rating:

$ uv run mypy typo_check.py
typo_check.py:9: error: "Review" has no attribute "ratng"; maybe "rating"?  [attr-defined]
Found 1 error in 1 file (checked 1 source file)

Leaving out the parentheses

The third mistake is leaving out the parentheses when you call a method:

review.is_positive == True
False

This gives False, because without parentheses, review.is_positive is the method itself, not its return value.

09Recap and challenge: classes, methods, special methods and inheritance

You started with a review in three separate pieces, and put its data and the code that uses it into one class. A class defines a new kind of object, and calling the class creates an instance of it, with __init__ giving each new instance its attributes. A method is a function in the class, and it receives the object it is called on as self, which is why review.is_positive() and Review.is_positive(review) are the same call.

Special methods, like __repr__, __eq__ and __add__, are the ones Python calls itself, and that is how the same + sign works for numbers and for strings. In fact, every value in Python is an object of some class. And with inheritance, a subclass starts with everything its base class has, and can add to it or change it, which is why a RatingError is still a ValueError.

Your challenge

Write a class called Cart:

  1. Its __init__ should create an empty list of items.
  2. Give it a method called add, which takes a name and a price.
  3. Give it a method called total, and a __repr__.
  4. Then add pasta at $12.50 and soup at $6.00, and check that the total is 18.5.

Looking ahead

The Review class needed three lines in __init__ that just copied the arguments onto self, plus a __repr__, and it still has no __eq__. That is a lot of code for a class whose main purpose is to store data. Python can generate all of those parts for you, from a list of the fields.