Learning Goals
3 minBy the end of this lesson you can:
- Define a method inside a class — same as a function, but indented under
class. - Explain what
selfis and why it's the first parameter of every method. - Call a method on an instance with
obj.method(). - Have a method read and modify the instance's own attributes.
Warm-Up · You've Been Calling Methods For 50 Lessons
5 minEvery one of these is a method call on an instance:
"hello".upper() # str method [1, 2].append(3) # list method {"a": 1}.keys() # dict method {1, 2}.union({3}) # set method
The function lives inside the class (str, list, etc.). When you call x.upper(), Python looks up upper on the class and runs it with x as the first parameter. Today we write our own.
A method is a function that lives inside a class. The first parameter — by convention named self — is the instance Python passes in automatically when you write obj.method().
New Concept · self and dot-call
14 minThe first method
class Hero: def greet(self): print("Hello, world!")
Two things to notice. (1) def greet(self): is indented under class Hero:. (2) The first parameter is self — that's the instance.
hero = Hero() hero.greet() # → Hello, world!
Python interprets hero.greet() as roughly Hero.greet(hero) — it automatically passes hero as the first argument. That's what self is: the "owner" instance.
Reading attributes inside a method
class Hero: def greet(self): print(f"Hi, I'm {self.name}.") aisyah = Hero() aisyah.name = "Aisyah" aisyah.greet() # → Hi, I'm Aisyah. wei_jie = Hero() wei_jie.name = "Wei Jie" wei_jie.greet() # → Hi, I'm Wei Jie.
Same method, two instances, two different printouts — because self.name reads from this hero, not some global.
Modifying attributes inside a method
class Hero: def take_damage(self, amount): self.hp -= amount print(f"{self.name} takes {amount} damage. HP now {self.hp}.") aisyah = Hero() aisyah.name = "Aisyah" aisyah.hp = 100 aisyah.take_damage(30) # → Aisyah takes 30 damage. HP now 70. aisyah.take_damage(20) # → Aisyah takes 20 damage. HP now 50.
Two parameters in the definition (self, amount); one argument at the call (30). Python fills self automatically — you only pass the extras.
Methods with return values
class Hero: def is_alive(self): return self.hp > 0
Same as any function — use return. Pure helpers like is_alive typically return a value and don't modify the instance.
The shape
class Hero:
def method_name(self, other_args):
# use self.attribute to read/write THIS instance's data
# may take more arguments, may return a value
hero = Hero()
hero.method_name(extra_args) # self filled in automaticallyWhat if you forget self?
You'll see one of two errors. Most common: TypeError: greet() takes 0 positional arguments but 1 was given — meaning Python passed in self but your method has no parameter to catch it.
class Bad: def greet(): # ❌ no self! print("hi") Bad().greet() # TypeError: greet() takes 0 positional arguments but 1 was given
Always include self as the first parameter. It's not a Python keyword — you could technically name it anything — but everyone uses self, so don't fight it.
Worked Example · The Combat-Ready Hero
12 minSave as combat.py:
# combat.py — Hero with methods class Hero: def greet(self): print(f"I am {self.name}. HP {self.hp}.") def take_damage(self, amount): self.hp -= amount if self.hp < 0: self.hp = 0 print(f" {self.name} takes {amount}. HP now {self.hp}.") def heal(self, amount): self.hp += amount if self.hp > self.max_hp: self.hp = self.max_hp print(f" {self.name} heals to {self.hp}.") def is_alive(self): return self.hp > 0 # Setup aisyah = Hero() aisyah.name = "Aisyah" aisyah.hp = 100 aisyah.max_hp = 100 wei_jie = Hero() wei_jie.name = "Wei Jie" wei_jie.hp = 50 wei_jie.max_hp = 80 # Play aisyah.greet() wei_jie.greet() print("\n--- Battle ---") aisyah.take_damage(30) wei_jie.take_damage(60) # would go below 0; clamped wei_jie.heal(40) print() for h in (aisyah, wei_jie): status = "alive" if h.is_alive() else "dead" print(f"{h.name}: HP {h.hp}/{h.max_hp} — {status}")
Output
I am Aisyah. HP 100. I am Wei Jie. HP 50. --- Battle --- Aisyah takes 30. HP now 70. Wei Jie takes 60. HP now 0. Wei Jie heals to 40. Aisyah: HP 70/100 — alive Wei Jie: HP 40/80 — alive
Read the diff
Four methods, four small jobs. greet only reads. take_damage and heal read and write — they modify the instance. is_alive just returns a boolean. Same Hero class, four behaviours. The data lives on each instance (self.hp is per-hero); the code lives on the class (same for everyone).
Try It Yourself
13 minDefine Counter with a value attribute (start at 0) and two methods: increment() and reset(). Test by making two counters and confirming they're independent.
Hint
class Counter: def increment(self): self.value += 1 def reset(self): self.value = 0 a = Counter(); a.value = 0 b = Counter(); b.value = 0 a.increment(); a.increment(); a.increment() b.increment() print(a.value, b.value) # → 3 1 a.reset() print(a.value, b.value) # → 0 1
Each counter keeps its own value. Tomorrow's __init__ will set the initial 0 automatically.
Add a method shout() to Hero that calls greet() three times in a row. Use self.greet() inside.
Hint
class Hero: def greet(self): print(f"Hi, I'm {self.name}!") def shout(self): for _ in range(3): self.greet() h = Hero() h.name = "Aisyah" h.shout() # Hi, I'm Aisyah! # Hi, I'm Aisyah! # Hi, I'm Aisyah!
Methods can call other methods on the same instance — just use self.other().
Add three methods to Hero: pick_up(item) appends to self.inventory; has(item) returns whether the item is in inventory; drop(item) removes it if present (silently no-op if not).
Hint
class Hero: def pick_up(self, item): self.inventory.append(item) def has(self, item): return item in self.inventory def drop(self, item): if item in self.inventory: self.inventory.remove(item) h = Hero(); h.inventory = [] h.pick_up("sword") h.pick_up("potion") print(h.has("sword")) # → True h.drop("sword") print(h.has("sword")) # → False
This is the same to-do-list pattern from PY-L2-01, wrapped in a class. Methods make the "what the inventory can do" obvious from the outside.
Mini-Challenge · Bank Account
8 minBuild bank.py. Define Account with these attributes and methods:
- Attributes:
holder(name),balance(number, starts 0). deposit(amount)— adds to balance. Reject negative amounts with a printed warning.withdraw(amount)— subtracts from balance. Reject if amount > balance, with a warning.show()— prints the holder name and current balance.transfer_to(other, amount)— withdraws from self, deposits toother.
Test with two accounts: deposit, withdraw, transfer, show.
Show one possible solution
# bank.py — Account class with methods class Account: def deposit(self, amount): if amount < 0: print(f" ! Can't deposit a negative amount: {amount}") return self.balance += amount def withdraw(self, amount): if amount > self.balance: print(f" ! Insufficient funds: have {self.balance}, want {amount}") return self.balance -= amount def show(self): print(f" {self.holder}: RM {self.balance:.2f}") def transfer_to(self, other, amount): if amount > self.balance: print(f" ! Transfer of {amount} > {self.holder}'s balance") return self.withdraw(amount) other.deposit(amount) print(f" Transferred {amount} from {self.holder} to {other.holder}") # Test a = Account(); a.holder = "Aisyah"; a.balance = 0 b = Account(); b.holder = "Wei Jie"; b.balance = 0 a.deposit(500) b.deposit(200) a.transfer_to(b, 150) a.withdraw(1000) # rejected a.show() b.show()
Non-negotiables: a transfer_to method that calls self.withdraw() and other.deposit() — methods calling methods on different instances. The validation lives in the methods, not the caller — a hallmark of well-encapsulated code.
Recap
3 minA method is a function inside a class. Always include self as the first parameter — Python passes the instance there automatically. Inside the method, use self.attr to read or write attributes on the calling instance. Methods can call other methods (self.other()) and operate on multiple instances (other.deposit(...) in a transfer). Same class, different data per instance — the foundation of OOP.
Vocabulary Card
- method
- A function defined inside a class. Lives on the class; runs on an instance.
- self
- The conventional name for the first parameter — the calling instance. Python fills it in automatically.
- obj.method()
- How you call a method. Equivalent to
ClassName.method(obj)behind the scenes. - method chain
- Inside a method,
self.other()calls another method on the same instance.
Homework
4 minBuild shopping_cart.py. Define a Cart class with attributes items (list of (name, price) tuples) and these methods:
add(name, price)— append a tuple.remove(name)— remove the first matching item.total()— return the sum of all prices.show()— print each item on its own line plus the total.apply_discount(percent)— reduce every price by that percent.
Test all five.
Sample · shopping_cart.py
# shopping_cart.py — Cart with five methods class Cart: def add(self, name, price): self.items.append((name, price)) def remove(self, name): for i, (n, _) in enumerate(self.items): if n == name: del self.items[i] return def total(self): return sum(price for _, price in self.items) def show(self): print("Cart") print("-" * 20) for name, price in self.items: print(f" {name:<14}{price:>5.2f}") print("-" * 20) print(f" {'Total':<14}{self.total():>5.2f}") def apply_discount(self, percent): factor = 1 - percent / 100 self.items = [(n, p * factor) for n, p in self.items] # Test c = Cart() c.items = [] c.add("nasi lemak", 8.0) c.add("teh tarik", 3.5) c.add("roti canai", 3.5) c.show() c.remove("teh tarik") c.show() c.apply_discount(10) # 10% off everything c.show()
Non-negotiables: five working methods, attribute access via self.items, and the discount rebuilds the list via comprehension. The Cart is now a self-contained object — call its methods and the data inside takes care of itself.