Instance Methods¶
Instance methods are functions defined inside a class that operate on instance-specific data via self. They are the most common method type --- for alternatives that operate on the class or neither, see Class Methods and Static Methods.
Mental Model
An instance method is just a regular function that lives on a class. When you call obj.method(), Python's descriptor protocol binds obj as the first argument (self), turning a plain function into a bound method. There is no special "method" type in Python -- it is functions plus attribute lookup all the way down.
Core Idea — Methods Are Not Special Syntax
Instance methods are just functions stored on the class. When you access one through an instance, Python's descriptor protocol runs:
text
function.__get__(instance, class) → bound method
This is why self is passed automatically — the bound method captures the
instance. The same mechanism underlies all three method types:
| Type | Descriptor behavior | Receives |
|---|---|---|
| Instance method | function.__get__(obj, cls) → bound to instance |
self |
@classmethod |
classmethod.__get__(obj, cls) → bound to class |
cls |
@staticmethod |
staticmethod.__get__(obj, cls) → raw function |
nothing |
Methods are not a language feature — they are a **side effect of attribute access
- descriptor binding**.
What are Instance Methods¶
1. Functions in Class¶
```python class Student: def init(self, name, courses): self.name = name self.courses = courses
def add_course(self, course): # Instance method
self.courses.append(course)
```
2. Receive self¶
First parameter is always self.
3. Operate on Instance¶
Methods modify or access instance attributes.
The self Parameter¶
1. Reference to Instance¶
```python class Student: def greet(self): print(f"Hello, I'm {self.name}")
a = Student("Lee") a.greet() # self = a ```
2. Automatic Passing¶
```python
These are equivalent:¶
a.greet() Student.greet(a) ```
3. Access Instance Data¶
python
def get_info(self):
return f"{self.name}, {self.major}"
Defining Methods¶
1. Basic Method¶
```python class Student: def init(self, name, courses): self.name = name self.courses = courses
def add_course(self, course):
if course not in self.courses:
self.courses.append(course)
```
2. Multiple Parameters¶
python
def enroll(self, course, semester):
enrollment = {
'course': course,
'semester': semester
}
self.enrollments.append(enrollment)
3. Return Values¶
python
def has_course(self, course):
return course in self.courses
Method Examples¶
1. Getter Method¶
```python class Student: def init(self, name): self.name = name
def get_name(self):
return self.name
```
2. Setter Method¶
python
def set_name(self, name):
if name:
self.name = name
3. Action Method¶
python
def drop_course(self, course):
if course in self.courses:
self.courses.remove(course)
Calling Methods¶
1. Via Instance¶
python
student = Student("Lee", ["Math"])
student.add_course("Physics")
2. Via Class¶
```python Student.add_course(student, "Physics")
Equivalent but not idiomatic¶
```
3. Chaining Methods¶
Instance methods can support method chaining by returning self (mutable style) or a new object of the same type (immutable style).
```python class Counter: def init(self): self.count = 0
def increment(self):
self.count += 1
return self # Return self for chaining
def reset(self):
self.count = 0
return self
c = Counter().increment().increment().reset() ```
self Mechanics¶
1. Must Use self¶
```python
WRONG¶
def add_course(self, course): courses.append(course) # NameError!
CORRECT¶
def add_course(self, course): self.courses.append(course) ```
2. Access Attributes¶
python
def print_info(self):
print(f"Name: {self.name}")
print(f"Major: {self.major}")
3. Call Other Methods¶
python
def enroll_and_notify(self, course):
self.add_course(course)
self.send_notification()
Instance vs Class Variables¶
1. Accessing Instance Variable¶
```python class Student: def init(self, name): self.name = name # instance variable
def greet(self):
return f"Hello, {self.name}"
```
2. Accessing Class Variable¶
```python class Student: university = 'Yonsei' # class variable
def get_university(self):
return Student.university # or self.university
```
3. Modifying Class Variable¶
```python def change_university(self, new_name): self.university = new_name # WRONG - creates instance attr
@classmethod def change_university(cls, new_name): cls.university = new_name # CORRECT ```
Method vs Function¶
1. Function Attribute¶
```python def f(): return 1
print(f) #
2. Method Attribute¶
```python import numpy as np x = np.array([1, 2, 3])
print(x.sum) #
3. Bound Methods¶
```python class Student: def greet(self): return "Hello"
a = Student()
print(Student.greet) #
"Bound" means the function already knows which object (self) it will use. Student.greet is just a function — nobody's self is attached. a.greet is a bound method — self is fixed to a, so calling a.greet() automatically passes a as the first argument.
This binding happens at access time, not definition time, because functions are descriptors --- they implement a __get__ method. When you access a function through an instance (a.greet), Python calls function.__get__(a, Student), which produces a bound method. This same mechanism underlies @classmethod (binds to the class) and @staticmethod (disables binding entirely).
| Expression | What it is | self attached? |
|---|---|---|
Student.greet |
function (unbound) | No |
a.greet |
bound method | Yes — fixed to a |
a.greet() |
calls Student.greet(a) |
Automatic |
A bound method is a lightweight wrapper that holds two pointers: __func__ (the shared function) and __self__ (the bound instance). A new wrapper is created on every access --- a.greet is a.greet is False --- but all wrappers share the same underlying function object.
```python a = Student() b = Student()
print(a.greet is b.greet) # False — different wrappers print(a.greet.func is b.greet.func) # True — same function print(a.greet.self is a) # True — bound to a ```
| Type | Receives | Bound to | Use case |
|---|---|---|---|
| Instance method | self |
instance | Object behavior and state |
| Class method | cls |
class | Shared config, alt constructors |
| Static method | nothing | nothing | Utility, no state needed |
Common Patterns¶
1. Validation¶
python
def set_age(self, age):
if 0 <= age <= 120:
self.age = age
else:
raise ValueError("Invalid age")
2. Computation¶
```python class Rectangle: def init(self, width, height): self.width = width self.height = height
def area(self):
return self.width * self.height
```
3. State Change¶
```python class Car: def init(self, speed): self.speed = speed
def accelerate(self, amount):
self.speed += amount
```
Helper Methods¶
1. Private Methods¶
```python class Student: def process(self): self._validate() self._compute()
def _validate(self): # "private" by convention
if not self.name:
raise ValueError("Name required")
def _compute(self):
pass
```
2. Public Interface¶
python
def enroll(self, course):
self._validate_prerequisites(course)
self._add_to_schedule(course)
self._update_credits()
3. Internal Logic¶
Keep complex logic in separate methods.
Method Signatures¶
1. No Parameters¶
python
def reset(self):
self.count = 0
2. With Parameters¶
python
def add(self, item, priority=0):
self.items.append((item, priority))
3. Variable Arguments¶
python
def add_multiple(self, *courses):
for course in courses:
self.add_course(course)
Key Takeaways¶
- Instance methods operate on instance data.
- First parameter is always
self. - Access instance attributes via
self. - Call methods via instance:
obj.method(). - Use
_methodfor internal helpers.
Exercises¶
Exercise 1.
Create a Stack class with instance methods push(item), pop(), peek() (returns top without removing), and is_empty(). All methods operate on self._items (a list). Demonstrate the full lifecycle: push several items, peek, pop, check empty.
Solution to Exercise 1
class Stack:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
if self.is_empty():
raise IndexError("pop from empty stack")
return self._items.pop()
def peek(self):
if self.is_empty():
raise IndexError("peek at empty stack")
return self._items[-1]
def is_empty(self):
return len(self._items) == 0
s = Stack()
s.push("a")
s.push("b")
s.push("c")
print(s.peek()) # c
print(s.pop()) # c
print(s.pop()) # b
print(s.is_empty()) # False
Exercise 2.
Write a StringProcessor class with self.text. Add instance methods to_upper(), to_lower(), reverse(), and word_count(). Each method should return a new StringProcessor so methods can be chained: StringProcessor("Hello World").to_upper().reverse().
Solution to Exercise 2
class StringProcessor:
def __init__(self, text):
self.text = text
def to_upper(self):
return StringProcessor(self.text.upper())
def to_lower(self):
return StringProcessor(self.text.lower())
def reverse(self):
return StringProcessor(self.text[::-1])
def word_count(self):
return len(self.text.split())
def __repr__(self):
return f"StringProcessor({self.text!r})"
result = StringProcessor("Hello World").to_upper().reverse()
print(result) # StringProcessor('DLROW OLLEH')
print(StringProcessor("one two three").word_count()) # 3
Exercise 3.
Build a Rectangle class with width and height. Add instance methods area(), perimeter(), is_square(), and scale(factor) (returns a new Rectangle). Show that scale returns a new object while the original is unchanged. Also show the difference between calling via instance (r.area()) and via class (Rectangle.area(r)).
Solution to Exercise 3
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
def is_square(self):
return self.width == self.height
def scale(self, factor):
return Rectangle(self.width * factor, self.height * factor)
def __repr__(self):
return f"Rectangle({self.width}, {self.height})"
r = Rectangle(4, 6)
r2 = r.scale(2)
print(r) # Rectangle(4, 6) — unchanged
print(r2) # Rectangle(8, 12)
# Two ways to call
print(r.area()) # 24
print(Rectangle.area(r)) # 24 — explicit self
Exercise 4.
Examine the following code and predict what each print outputs. Explain why Student.greet and s.greet have different types, and what the descriptor protocol has to do with it.
```python class Student: def greet(self): return f"Hi, I'm {self.name}"
s = Student() s.name = "Alice"
print(type(Student.greet)) print(type(s.greet)) print(Student.greet(s)) print(s.greet())
f = s.greet print(f()) ```
Solution to Exercise 4
class Student:
def greet(self):
return f"Hi, I'm {self.name}"
s = Student()
s.name = "Alice"
print(type(Student.greet)) # <class 'function'>
print(type(s.greet)) # <class 'method'>
print(Student.greet(s)) # Hi, I'm Alice
print(s.greet()) # Hi, I'm Alice
f = s.greet
print(f()) # Hi, I'm Alice — f is a bound method, self is captured
# Student.greet is a plain function (accessed on the class).
# s.greet is a bound method (accessed on an instance).
# The difference exists because functions are descriptors:
# function.__get__(s, Student) returns a bound method with self=s.
# That is why s.greet() works without passing self explicitly.
Exercise 5.
Trace through the following code and predict all three print outputs. Explain how drop and add modify the instance's state through self.
```python class Student: def init(self, name, subjects): self.name = name self.subjects = subjects
def drop(self, subject):
if subject in self.subjects:
self.subjects.remove(subject)
def add(self, subject):
if subject not in self.subjects:
self.subjects.append(subject)
a = Student("Kim", ["Cal", "Linear Alg"]) print(a.subjects) a.drop("Cal") print(a.subjects) a.add("Python") print(a.subjects) ```
Solution to Exercise 5
class Student:
def __init__(self, name, subjects):
self.name = name
self.subjects = subjects
def drop(self, subject):
if subject in self.subjects:
self.subjects.remove(subject)
def add(self, subject):
if subject not in self.subjects:
self.subjects.append(subject)
a = Student("Kim", ["Cal", "Linear Alg"])
print(a.subjects) # ['Cal', 'Linear Alg']
a.drop("Cal")
print(a.subjects) # ['Linear Alg']
a.add("Python")
print(a.subjects) # ['Linear Alg', 'Python']
# How it works:
# a.drop("Cal") calls Student.drop(a, "Cal").
# Inside drop, self is a, so self.subjects is a.subjects.
# "Cal" is in the list, so remove() mutates the list in place.
#
# a.add("Python") calls Student.add(a, "Python").
# "Python" is not in the list, so append() adds it.
#
# Both methods modify self.subjects (the same list object)
# through self. They return None — the mutation happens
# as a side effect, not via a return value.