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']
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
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'
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:
- Its
__init__should create an empty list of items. - Give it a method called
add, which takes a name and a price. - Give it a method called
total, and a__repr__. - 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.
Key terms: Classes and Objects
The words this lesson introduces, each in one line. The module’s glossary collects them all, and the AI glossary has every term across the courses.
self- The conventional first parameter of an instance method: the object the method was called on.
review.is_positive()isReview.is_positive(review). - attribute
- A value stored on an object, reached with a dot:
review.rating.vars(review)shows an object's attributes as a dictionary. - class
- A definition of a new kind of object:
class Review:. Calling it,Review(...), creates an instance. - inheritance
- Defining a class from another, so that it starts with everything the other has:
class VerifiedReview(Review):. - instance
- An object created from a class:
review = Review("kettle", 5, "…"). - method
- A function defined inside a class, called on an object with a dot:
review.tokens(). - special method
- A method with two underscores on each side of its name, which Python calls itself:
__init__,__repr__,__eq__,__add__. - subclass
- A class defined from a base class:
RatingErroris a subclass ofValueError.
Quiz: Classes and Objects
Your first pick on each question is the one that counts. Getting one wrong here is how the lesson sticks.