Instance methods are functions that belong to objects and operate on their individual data through the special self parameter. They are the primary way objects exhibit behavior and interact with their own attributes and other objects. Understanding instance methods is crucial for effective object-oriented programming in Python.
An instance method is a function defined inside a class that operates on individual objects (instances) of that class. Every instance method must have self as its first parameter, which Python automatically passes when you call the method.
class Counter:
def __init__(self, start=0):
self.value = start # Instance attribute
def increment(self): # Instance method
"""Increase counter by 1."""
self.value += 1 # Access instance attribute through self
return self.value
def get_value(self): # Instance method
"""Return current counter value."""
return self.value # Access instance attribute through self
# Create instances and call methods
counter1 = Counter(10)
counter2 = Counter(0)
print(counter1.increment()) # 11 - Python automatically passes counter1 as self
print(counter2.increment()) # 1 - Python automatically passes counter2 as self
print(counter1.get_value()) # 11 - Each object has its own state
print(counter2.get_value()) # 1The self parameter is Python's way of referring to the specific object instance that the method is called on:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
"""Instance method using self to access attributes."""
return f"Hi, I'm {self.name} and I'm {self.age} years old."
def have_birthday(self):
"""Instance method that modifies state."""
self.age += 1
return f"{self.name} is now {self.age} years old!"
def greet(self, other_person):
"""Instance method that takes additional parameters."""
return f"{self.name} says hello to {other_person.name}!"
alice = Person("Alice", 25)
bob = Person("Bob", 30)
# Python automatically passes the object as self
print(alice.introduce()) # Hi, I'm Alice and I'm 25 years old.
print(bob.introduce()) # Hi, I'm Bob and I'm 30 years old.
alice.have_birthday()
print(alice.introduce()) # Hi, I'm Alice and I'm 26 years old.
print(alice.greet(bob)) # Alice says hello to Bob!
# This is what Python does behind the scenes:
# alice.introduce() is equivalent to Person.introduce(alice)class Example:
def __init__(self, value):
self.value = value
def show_self(self):
print(f"self is: {self}")
print(f"self.value is: {self.value}")
print(f"id(self) is: {id(self)}")
obj1 = Example("first")
obj2 = Example("second")
print(f"obj1 is: {obj1}")
print(f"id(obj1) is: {id(obj1)}")
obj1.show_self()
print()
print(f"obj2 is: {obj2}")
print(f"id(obj2) is: {id(obj2)}")
obj2.show_self()
# Output shows that self refers to the specific object instanceMethods that return information about the object's state:
class BankAccount:
def __init__(self, account_number, balance=0):
self.account_number = account_number
self._balance = balance # Protected attribute
self.transaction_history = []
def get_balance(self):
"""Return current account balance."""
return self._balance
def get_account_number(self):
"""Return account number."""
return self.account_number
def get_transaction_count(self):
"""Return number of transactions."""
return len(self.transaction_history)
def get_last_transaction(self):
"""Return most recent transaction, or None if no transactions."""
if self.transaction_history:
return self.transaction_history[-1]
return None
account = BankAccount("123456789", 1000)
print(account.get_balance()) # 1000
print(account.get_transaction_count()) # 0Methods that change the object's state:
class Student:
def __init__(self, name, student_id):
self.name = name
self.student_id = student_id
self.grades = {}
self.enrollment_status = "active"
def set_name(self, new_name):
"""Update student's name."""
if not new_name or not isinstance(new_name, str):
raise ValueError("Name must be a non-empty string")
old_name = self.name
self.name = new_name
return f"Name changed from {old_name} to {new_name}"
def add_grade(self, subject, grade):
"""Add a grade for a subject."""
if not 0 <= grade <= 100:
raise ValueError("Grade must be between 0 and 100")
self.grades[subject] = grade
return f"Added {subject}: {grade}"
def update_grade(self, subject, new_grade):
"""Update existing grade for a subject."""
if subject not in self.grades:
raise KeyError(f"No existing grade for {subject}")
if not 0 <= new_grade <= 100:
raise ValueError("Grade must be between 0 and 100")
old_grade = self.grades[subject]
self.grades[subject] = new_grade
return f"Updated {subject} from {old_grade} to {new_grade}"
def withdraw(self):
"""Change enrollment status to withdrawn."""
if self.enrollment_status == "withdrawn":
return "Student is already withdrawn"
self.enrollment_status = "withdrawn"
return f"{self.name} has been withdrawn"
student = Student("Alice", "12345")
print(student.add_grade("Math", 95)) # Added Math: 95
print(student.update_grade("Math", 98)) # Updated Math from 95 to 98Methods that perform operations or behaviors:
class ShoppingCart:
def __init__(self):
self.items = {} # {item_name: {'price': price, 'quantity': quantity}}
self.discount_rate = 0.0
def add_item(self, item_name, price, quantity=1):
"""Add items to the cart."""
if item_name in self.items:
self.items[item_name]['quantity'] += quantity
else:
self.items[item_name] = {'price': price, 'quantity': quantity}
return f"Added {quantity} {item_name}(s) to cart"
def remove_item(self, item_name, quantity=None):
"""Remove items from the cart."""
if item_name not in self.items:
raise KeyError(f"{item_name} not in cart")
if quantity is None or quantity >= self.items[item_name]['quantity']:
# Remove all of this item
removed_quantity = self.items[item_name]['quantity']
del self.items[item_name]
return f"Removed all {removed_quantity} {item_name}(s) from cart"
else:
# Remove partial quantity
self.items[item_name]['quantity'] -= quantity
return f"Removed {quantity} {item_name}(s) from cart"
def apply_discount(self, discount_rate):
"""Apply a discount to the cart."""
if not 0 <= discount_rate <= 1:
raise ValueError("Discount rate must be between 0 and 1")
self.discount_rate = discount_rate
return f"Applied {discount_rate * 100}% discount"
def checkout(self):
"""Process the cart and clear it."""
if not self.items:
raise ValueError("Cannot checkout empty cart")
subtotal = sum(item['price'] * item['quantity'] for item in self.items.values())
discount_amount = subtotal * self.discount_rate
total = subtotal - discount_amount
# Clear the cart
items_purchased = len(self.items)
self.items.clear()
self.discount_rate = 0.0
return {
'items_purchased': items_purchased,
'subtotal': subtotal,
'discount': discount_amount,
'total': total
}
cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Mouse", 29.99, 2)
cart.apply_discount(0.1)
receipt = cart.checkout()
print(f"Total: ${receipt['total']:.2f}") # Total with 10% discountMethods that return boolean values or answer questions about the object:
class Rectangle:
def __init__(self, width, height):
if width <= 0 or height <= 0:
raise ValueError("Width and height must be positive")
self.width = width
self.height = height
def is_square(self):
"""Check if this rectangle is a square."""
return self.width == self.height
def is_larger_than(self, other_rectangle):
"""Check if this rectangle has a larger area than another."""
return self.get_area() > other_rectangle.get_area()
def can_contain(self, other_rectangle):
"""Check if this rectangle can contain another rectangle."""
return (self.width >= other_rectangle.width and
self.height >= other_rectangle.height)
def is_similar_to(self, other_rectangle, tolerance=0.1):
"""Check if this rectangle has similar proportions to another."""
ratio1 = self.width / self.height
ratio2 = other_rectangle.width / other_rectangle.height
return abs(ratio1 - ratio2) <= tolerance
def get_area(self):
"""Calculate area (helper method for comparisons)."""
return self.width * self.height
rect1 = Rectangle(4, 4)
rect2 = Rectangle(3, 6)
rect3 = Rectangle(2, 2)
print(rect1.is_square()) # True
print(rect2.is_square()) # False
print(rect2.is_larger_than(rect1)) # True (18 > 16)
print(rect1.can_contain(rect3)) # True
print(rect1.is_similar_to(rect3)) # True (both are squares)Methods can return self to enable fluent interfaces and method chaining:
class TextProcessor:
def __init__(self, text=""):
self.text = text
def add_text(self, new_text):
"""Add text and return self for chaining."""
self.text += new_text
return self
def uppercase(self):
"""Convert to uppercase and return self for chaining."""
self.text = self.text.upper()
return self
def replace(self, old, new):
"""Replace text and return self for chaining."""
self.text = self.text.replace(old, new)
return self
def trim(self):
"""Remove whitespace and return self for chaining."""
self.text = self.text.strip()
return self
def get_result(self):
"""Return the final result."""
return self.text
def __str__(self):
"""String representation."""
return self.text
# Method chaining in action
processor = TextProcessor(" hello world ")
result = (processor
.trim()
.uppercase()
.replace("WORLD", "PYTHON")
.add_text("!")
.get_result())
print(result) # HELLO PYTHON!
# Each method call returns the object, allowing chainingInstance methods can call other methods on the same object:
class Calculator:
def __init__(self):
self.value = 0
self.history = []
def _record_operation(self, operation, operand, result):
"""Private helper method to record operations."""
self.history.append(f"{operation} {operand} = {result}")
def add(self, number):
"""Add a number to the current value."""
old_value = self.value
self.value += number
self._record_operation(f"{old_value} +", number, self.value)
return self
def multiply(self, number):
"""Multiply current value by a number."""
old_value = self.value
self.value *= number
self._record_operation(f"{old_value} *", number, self.value)
return self
def power(self, exponent):
"""Raise current value to a power."""
old_value = self.value
self.value = self.value ** exponent
self._record_operation(f"{old_value} ^", exponent, self.value)
return self
def reset(self):
"""Reset calculator and clear history."""
self.value = 0
self.history.clear()
return self
def get_value(self):
"""Get current value."""
return self.value
def get_history(self):
"""Get operation history."""
return self.history.copy()
def undo_last(self):
"""Undo the last operation (simplified version)."""
if not self.history:
return self
# This is a simplified undo - in real implementation,
# you'd store previous values
last_op = self.history.pop()
print(f"Undoing: {last_op}")
return self
calc = Calculator()
calc.add(10).multiply(3).power(2)
print(f"Result: {calc.get_value()}") # 900
print("History:")
for operation in calc.get_history():
print(f" {operation}")Instance methods can accept parameters just like regular functions:
class DataProcessor:
def __init__(self):
self.data = []
def add_data(self, *values):
"""Add multiple values using *args."""
self.data.extend(values)
return f"Added {len(values)} values"
def filter_data(self, condition_func, keep_original=False):
"""Filter data using a function parameter."""
if keep_original:
return [x for x in self.data if condition_func(x)]
else:
self.data = [x for x in self.data if condition_func(x)]
return f"Filtered to {len(self.data)} values"
def transform_data(self, transform_func, **kwargs):
"""Transform data using function and keyword arguments."""
self.data = [transform_func(x, **kwargs) for x in self.data]
return f"Transformed {len(self.data)} values"
def get_stats(self, include_details=True):
"""Get statistics with optional details."""
if not self.data:
return "No data available"
stats = {
'count': len(self.data),
'sum': sum(self.data),
'average': sum(self.data) / len(self.data)
}
if include_details:
stats.update({
'min': min(self.data),
'max': max(self.data),
'range': max(self.data) - min(self.data)
})
return stats
def is_positive(x):
return x > 0
def multiply_by(x, factor=2):
return x * factor
processor = DataProcessor()
processor.add_data(1, -2, 3, -4, 5)
processor.filter_data(is_positive) # Keep only positive numbers
processor.transform_data(multiply_by, factor=3) # Multiply by 3
print(processor.get_stats()) # Statistics of [3, 9, 15]Methods should validate inputs and handle error conditions appropriately:
class BankAccount:
def __init__(self, account_number, initial_balance=0):
if not isinstance(account_number, str) or not account_number:
raise ValueError("Account number must be a non-empty string")
if not isinstance(initial_balance, (int, float)) or initial_balance < 0:
raise ValueError("Initial balance must be a non-negative number")
self.account_number = account_number
self._balance = float(initial_balance)
self._is_frozen = False
def deposit(self, amount):
"""Deposit money with validation."""
if self._is_frozen:
raise RuntimeError("Account is frozen")
if not isinstance(amount, (int, float)):
raise TypeError("Amount must be a number")
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self._balance += amount
return f"Deposited ${amount:.2f}. New balance: ${self._balance:.2f}"
def withdraw(self, amount):
"""Withdraw money with validation."""
if self._is_frozen:
raise RuntimeError("Account is frozen")
if not isinstance(amount, (int, float)):
raise TypeError("Amount must be a number")
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self._balance:
raise ValueError(f"Insufficient funds. Balance: ${self._balance:.2f}")
self._balance -= amount
return f"Withdrew ${amount:.2f}. New balance: ${self._balance:.2f}"
def freeze_account(self):
"""Freeze the account to prevent transactions."""
if self._is_frozen:
return "Account is already frozen"
self._is_frozen = True
return "Account has been frozen"
def unfreeze_account(self):
"""Unfreeze the account to allow transactions."""
if not self._is_frozen:
return "Account is not frozen"
self._is_frozen = False
return "Account has been unfrozen"
def get_balance(self):
"""Get current balance."""
return self._balance
def is_frozen(self):
"""Check if account is frozen."""
return self._is_frozen
# Using the bank account with error handling
try:
account = BankAccount("12345", 1000)
print(account.deposit(500)) # Success
print(account.withdraw(2000)) # Raises ValueError: insufficient funds
except ValueError as e:
print(f"Error: {e}")
try:
account.freeze_account()
account.deposit(100) # Raises RuntimeError: account frozen
except RuntimeError as e:
print(f"Error: {e}")Instance methods should have clear docstrings explaining their purpose, parameters, and return values:
class Temperature:
def __init__(self, celsius=0):
"""Initialize temperature in Celsius.
Args:
celsius (float): Temperature in Celsius. Defaults to 0.
Raises:
ValueError: If temperature is below absolute zero.
"""
if celsius < -273.15:
raise ValueError("Temperature cannot be below absolute zero (-273.15°C)")
self._celsius = celsius
def convert_to_fahrenheit(self):
"""Convert temperature to Fahrenheit.
Returns:
float: Temperature in Fahrenheit.
Example:
>>> temp = Temperature(25)
>>> temp.convert_to_fahrenheit()
77.0
"""
return (self._celsius * 9/5) + 32
def convert_to_kelvin(self):
"""Convert temperature to Kelvin.
Returns:
float: Temperature in Kelvin.
Example:
>>> temp = Temperature(25)
>>> temp.convert_to_kelvin()
298.15
"""
return self._celsius + 273.15
def is_freezing(self, substance="water"):
"""Check if substance would freeze at this temperature.
Args:
substance (str): The substance to check. Currently only supports "water".
Defaults to "water".
Returns:
bool: True if the substance would freeze, False otherwise.
Raises:
ValueError: If substance is not supported.
Example:
>>> temp = Temperature(-5)
>>> temp.is_freezing()
True
"""
freezing_points = {"water": 0}
if substance not in freezing_points:
raise ValueError(f"Freezing point for '{substance}' is not known")
return self._celsius <= freezing_points[substance]class EmailValidator:
def __init__(self):
self.valid_domains = {'gmail.com', 'yahoo.com', 'outlook.com'}
def _is_valid_format(self, email):
"""Private helper method to check email format."""
return '@' in email and '.' in email.split('@')[1]
def _is_allowed_domain(self, email):
"""Private helper method to check domain."""
domain = email.split('@')[1]
return domain in self.valid_domains
def validate(self, email):
"""Validate email address using helper methods."""
if not isinstance(email, str):
return False, "Email must be a string"
if not self._is_valid_format(email):
return False, "Invalid email format"
if not self._is_allowed_domain(email):
return False, "Domain not allowed"
return True, "Valid email"
validator = EmailValidator()
is_valid, message = validator.validate("user@gmail.com")
print(f"Valid: {is_valid}, Message: {message}")class GameCharacter:
def __init__(self, name, health=100):
self.name = name
self.max_health = health
self.health = health
self.level = 1
self.experience = 0
self.is_alive = True
def take_damage(self, damage):
"""Apply damage to character."""
if not self.is_alive:
return f"{self.name} is already defeated"
self.health = max(0, self.health - damage)
if self.health == 0:
self.is_alive = False
return f"{self.name} has been defeated!"
return f"{self.name} takes {damage} damage. Health: {self.health}/{self.max_health}"
def heal(self, amount):
"""Restore health to character."""
if not self.is_alive:
return f"Cannot heal {self.name} - character is defeated"
old_health = self.health
self.health = min(self.max_health, self.health + amount)
healed = self.health - old_health
return f"{self.name} healed for {healed} points. Health: {self.health}/{self.max_health}"
def gain_experience(self, exp):
"""Add experience and handle level ups."""
if not self.is_alive:
return f"{self.name} cannot gain experience while defeated"
self.experience += exp
messages = [f"{self.name} gains {exp} experience"]
# Check for level up (simplified: 100 exp per level)
while self.experience >= self.level * 100:
self.experience -= self.level * 100
self.level += 1
self.max_health += 20
self.health = self.max_health # Full heal on level up
messages.append(f"Level up! {self.name} is now level {self.level}")
return ". ".join(messages)
character = GameCharacter("Hero")
print(character.take_damage(30)) # Hero takes 30 damage
print(character.heal(10)) # Hero healed for 10 points
print(character.gain_experience(150)) # Level up occursInstance methods are essential because they:
- Encapsulate behavior: Keep related functionality with the data it operates on
- Maintain object state: Allow objects to modify their own attributes safely
- Provide interfaces: Define how other code can interact with objects
- Enable polymorphism: Different classes can implement the same method names differently
- Support data validation: Control how object data is accessed and modified
- Model real-world actions: Let objects perform actions that make sense for their type
As emphasized in the python-org README, instance methods are fundamental to object-oriented programming because they define what objects can do. They transform simple data containers into active entities that can:
- Respond to requests: Through getter methods
- Change their state: Through setter and action methods
- Answer questions: Through query methods
- Interact with other objects: Through methods that take other objects as parameters
- Validate their own data: Through internal validation logic
Mastering instance methods - including the self parameter, method types, parameter handling, and error management - is essential for creating robust, maintainable object-oriented Python code. They provide the mechanism for objects to exhibit intelligent behavior and maintain their own integrity.