Learning Goals
3 minBy the end of this lesson you can:
- Write a method inside a class and call it with
object.method(). - Explain what the dot passes in, and why every method starts with
self. - Choose between a method that changes the object and one that returns an answer.
- Add
__str__soprint(object)prints something useful.
Warm-Up · The Function That Lives Outside
5 minMia keeps a tabung — a savings tin. Predict the last line, then read the question underneath.
class Tabung: """A savings tin.""" def __init__(self, owner, balance=0): self.owner = owner self.balance = balance def deposit(tin, amount): tin.balance = tin.balance + amount Mia_tin = Tabung("Mia") deposit(Mia_tin, 5) deposit(Mia_tin, 2.50) print(Mia_tin.owner, "has $", Mia_tin.balance)
Answer
Output
Mia has $7.5
It works. That is the point — it works, and it is still the wrong shape.
depositIf you later write a Wallet class, do you need a second deposit function with a different name? And when someone reads Tabung, do they learn that a tin can take money in? The data is inside the class and the behaviour is outside it, so the two can drift apart.
New Concept · Functions That Live Inside
12 minMove the function inside the class, indent it, and rename its first parameter self. That is the entire change.
class Tabung: """A savings tin that looks after its own money.""" def __init__(self, owner, balance=0): self.owner = owner self.balance = balance def deposit(self, amount): """Add money to the tin.""" self.balance = self.balance + amount
A function written inside a class is called a method. You call it through an object:
Mia_tin = Tabung("Mia") Mia_tin.deposit(5) print(Mia_tin.balance)
Output
5
Mia_tin.deposit(5) looks like one argument. Python turns it into two: the object on the left of the dot becomes self, and 5 becomes amount. That is why every method has one more parameter in the def than you type at the call — and why self must be first.
Methods come in two flavours, and it is the same split as FUN-03: does it do something, or does it give something back?
def deposit(self, amount): """Change the object: the balance goes up.""" self.balance = self.balance + amount def can_afford(self, price): """Report an answer: True if there is enough money.""" return self.balance >= price
deposit changes the tin and returns nothing. can_afford changes nothing and returns True or False, so the caller decides what to do:
if Mia_tin.can_afford(4.50): print("Pizza is affordable") else: print("Save a bit more first")
A method may do both — change the object and report whether it worked. That is how you refuse an impossible action instead of allowing it.
def withdraw(self, amount): """Take money out. Return True if the tin had enough.""" if amount > self.balance: return False self.balance = self.balance - amount return True
Before, any line anywhere could write tin.balance = -50. Now there is one method that owns the rule "you cannot take out more than is in there", and it lives beside the data it protects. That is what people mean when they say behaviour belongs with the object.
One special method is worth knowing today. Print an object as it stands and Python shows you something unhelpful:
Output
<__main__.Tabung object at 0x000001C4F2A9E3D0>
Add a __str__ method — another dunder, like __init__ — that returns the text you want:
def __str__(self): """The line Python prints for this tin.""" return f"{self.owner}'s tabung: $ {self.balance:.2f}"
Output
Mia's tabung: $7.50
You never call __str__ yourself. print(Mia_tin) calls it for you — which will matter a great deal in the next lesson, when you print a whole list of objects.
Worked Example · A Tabung That Says No
12 minType this out and run it. Four methods, and the program underneath is only nine lines.
class Tabung: """A savings tin that looks after its own money.""" def __init__(self, owner, balance=0): self.owner = owner self.balance = balance def deposit(self, amount): """Add money to the tin.""" self.balance = self.balance + amount def withdraw(self, amount): """Take money out. Return True if the tin had enough.""" if amount > self.balance: return False self.balance = self.balance - amount return True def can_afford(self, price): """Return True if the tin holds at least price.""" return self.balance >= price def __str__(self): """The line Python prints for this tin.""" return f"{self.owner}'s tabung: $ {self.balance:.2f}" Mia_tin = Tabung("Mia") Mia_tin.deposit(5) Mia_tin.deposit(2.50) print(Mia_tin) if Mia_tin.withdraw(4.50): print("Bought pizza.") else: print("Not enough money.") print(Mia_tin) if Mia_tin.withdraw(20): print("Bought a book.") else: print("Not enough for the book - saving instead.") print(Mia_tin) print("Can afford hot chocolate?", Mia_tin.can_afford(2.50))
Output
Mia's tabung: $7.50 Bought pizza. Mia's tabung: $3.00 Not enough for the book - saving instead. Mia's tabung: $3.00 Can afford hot chocolate? True
No arithmetic. No check for an empty tin. No formatting of dollar. All three live in the class, once, where they cannot be forgotten. The program below reads like a description of what happened, which is exactly what you want at the bottom of a file.
Now make a second tin — wei_jie_tin = Tabung("Leo", 50) — and call the same methods on it. Same class, same behaviour, completely separate money.
Try It Yourself
13 minAdd two methods to your Drink class: iced_price, which returns the price plus 50 cents if the drink is iced, and __str__, which returns a line such as "Iced cocoa - $3.50". Then just print the drink.
Hint
def iced_price(self): """Return the price, plus 50 cents if it is iced.""" if self.iced: return self.price + 0.50 return self.price
Write a Player class with name, hp (default 100) and potions (default 2). Give it four methods:
take_damage(amount)— lowershp, never below0.drink_potion()— adds 30 hp and uses one potion; returnsFalseif there are none left.is_alive()— returnsTrueorFalse.__str__— a status line.
Then write a short fight: take damage three times, drink a potion, and print the player after every step. Nothing outside the class should touch hp directly.
Mini-Challenge 🔥 · Debug: The Method That Does Nothing
8 minNoah's hero never loses HP, and the if is always true even when he should be defeated. Find three mistakes.
class Player:
"""One player in the game."""
def __init__(self, name, hp=100):
self.name = name
self.hp = hp
def take_damage(amount):
hp = hp - amount
def is_alive(self):
print(self.hp > 0)
hero = Player("Noah")
hero.take_damage(30)
if hero.is_alive:
print(hero.name, "fights on with", hero.hp, "HP")Answer
take_damagehas noself. The dot always passes the object, sohero.take_damage(30)sends two values into a method expecting one:TypeError: take_damage() takes 1 positional argument but 2 were given.hp = hp - amountis a local. Even withselffixed, it never touches the object. It must beself.hp = self.hp - amount— the same trap as FUN-05, wearing a different hat.is_aliveprints instead of returning, so it hands backNone. And the call is missing its brackets:hero.is_aliveis the method itself, which is never empty, so theifis always true. It needsreturn self.hp > 0andif hero.is_alive():.
The rule behind all three: a method that decides something should return it, and a method that changes the object must say self. out loud.
Recap
3 minA method is a function defined inside a class. The object on the left of the dot arrives as self, which is why self is always the first parameter and never typed at the call. Methods that change the object write self.something = ...; methods that answer a question return a value; some do both, to refuse an action that is not allowed. __str__ returns the text Python prints for your object. Data and the rules about that data now live in the same place.
Vocabulary Card
- method
- A function defined inside a class, called as
object.method(). - self
- The object the method was called on, passed in automatically by the dot.
- state
- The data an object is currently holding — its attributes right now.
__str__- The method that returns the text
print(object)should show.
Homework
4 minGive your own class from OOP-02 three methods, one of each kind:
- One that changes the object.
- One that returns an answer and changes nothing.
- A
__str__that returns one tidy line.
Then write a short program that uses all three, and never reads or writes an attribute with a dot outside the class.
Hint: if you cannot find a "changes" method, ask what happens to your object over time — a book gets read, a bus fills up, a phone battery drops.
Sample · rak_buku.py
# rak_buku.py - a book that knows what to do with itself class Book: """One book on my shelf.""" def __init__(self, title, author, price, pages): self.title = title self.author = author self.price = price self.pages = pages self.page_now = 0 def read_pages(self, count): """Move the bookmark forward, never past the last page.""" self.page_now = min(self.pages, self.page_now + count) def is_finished(self): """Return True once the bookmark reaches the last page.""" return self.page_now >= self.pages def __str__(self): """One tidy line for this book.""" return f"{self.title} - page {self.page_now}/{self.pages} - $ {self.price:.2f}" komik = Book("Lat: Village Boy", "Lat", 24.00, 144) komik.read_pages(60) print(komik) komik.read_pages(200) print(komik) print("Finished?", komik.is_finished())
Output
Lat: Village Boy - page 60/144 - $24.00 Lat: Village Boy - page 144/144 - $24.00 Finished? True
The "never past the last page" rule sits inside read_pages, so no program using this class can ever put the bookmark on page 300. Your class will be different — the shape is what counts.