Previous | Tutorial index | Next
Understand the three types of methods and when to use each.
In Python, you can define three distinct types of methods within a class:
self as first parameter.cls as first parameter; use the @classmethod decorator.@staticmethod decorator; take no special first parameter.class MyClass:
# 1. Instance method
def instance_method(self):
return f"Instance: {self}"
# 2. Class method
@classmethod
def class_method(cls):
return f"Class: {cls}"
# 3. Static method
@staticmethod
def static_method():
return "Static: Utility function"
Key Insight: The difference lies in what they can access and how they are called. Understanding these differences is crucial for writing well-structured OOP code.
self as the first parameter (a reference to the instance).self.attribute).self.__class__ or ClassName.attribute.obj.method().class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def birthday(self):
"""Instance method that modifies the instance."""
self.age += 1
return f"Happy birthday, {self.name}! You are now {self.age}."
def display(self):
"""Instance method that reads instance state."""
return f"{self.name} is {self.age} years old."
p = Person("Alice", 30)
print(p.birthday()) # Happy birthday, Alice! You are now 31.
print(p.display()) # Alice is 31 years old.
@classmethod)cls as the first parameter (a reference to the class, not an instance).cls.attribute).ClassName.method() or on an instance: obj.method() (though this is less common).class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def from_birth_year(cls, name, birth_year, current_year=2024):
"""Factory method: create a Person from birth year."""
age = current_year - birth_year
return cls(name, age) # cls() creates an instance of the class
@classmethod
def from_string(cls, data_string):
"""Factory method: create a Person from "name,age" string."""
name, age = data_string.split(',')
return cls(name.strip(), int(age.strip()))
# Using the factory methods
p1 = Person.from_birth_year("Alice", 1990) # Creates Person instance
p2 = Person.from_string("Bob, 25") # Creates Person instance
print(p1.age) # 34 (assuming current_year=2024)
print(p2.age) # 25
Important: Using cls instead of hardcoding the class name (Person) ensures that subclasses work correctly. If you hardcode Person, a subclass would return a Person instance, not a subclass instance.
class Student:
total_students = 0
def __init__(self, name):
self.name = name
Student.total_students += 1
@classmethod
def get_total(cls):
"""Class method to retrieve the total student count."""
return f"Total students: {cls.total_students}"
@classmethod
def reset_count(cls):
"""Class method to reset the counter (useful for testing)."""
cls.total_students = 0
print(Student.get_total()) # Total students: 0
s1 = Student("Alice")
s2 = Student("Bob")
print(Student.get_total()) # Total students: 2
@staticmethod)self or cls).ClassName.method() or on an instance: obj.method().class MathUtils:
@staticmethod
def is_even(number):
"""Check if a number is even."""
return number % 2 == 0
@staticmethod
def factorial(n):
"""Calculate factorial recursively."""
if n < 0:
raise ValueError("Factorial not defined for negative numbers")
if n <= 1:
return 1
return n * MathUtils.factorial(n - 1)
# Called on the class
print(MathUtils.is_even(10)) # True
print(MathUtils.factorial(5)) # 120
class Product:
def __init__(self, name, price, sku):
self.name = name
self.price = price
self.sku = sku
@staticmethod
def validate_sku(sku):
"""Check if SKU is valid (e.g., exactly 8 alphanumeric characters)."""
return len(sku) == 8 and sku.isalnum()
@classmethod
def from_sku_string(cls, sku_string):
"""Factory method that uses the static validator."""
parts = sku_string.split('-')
if len(parts) == 3 and cls.validate_sku(parts[2]):
return cls(parts[0], float(parts[1]), parts[2])
raise ValueError("Invalid SKU format")
# Using the static method
print(Product.validate_sku("ABC12345")) # True
print(Product.validate_sku("ABC123")) # False
# Using the factory method (which uses the static method)
p = Product.from_sku_string("Laptop-999.99-ABC12345")
Why not just a standalone function? Static methods keep the utility function namespaced under the class, making the code more organized and self-documenting.
| Scenario | Which Method to Use | Why |
|---|---|---|
Operating on instance data (e.g., self.name, self.age) |
Instance Method | You need access to the instance's state. |
| Creating instances in different ways (alternative constructors) | Class Method | You need to return a new instance; cls ensures subclasses work correctly. |
| Modifying class-level data (e.g., counters, shared settings) | Class Method | You need to modify state shared by all instances. |
| A utility function that doesn't access any class or instance data | Static Method | It's just a helper; no need for self or cls. |
| A function that validates input for a class | Static Method | It doesn't need class/instance data, but it's logically part of the class. |
| A method that should be overridden in subclasses (polymorphism) | Instance or Class Method | Both can be overridden; choose based on what you need (instance or class data). |
| A method that works independently but is conceptually tied to the class | Static Method | Keeps related functions grouped together. |
Instance methods are inherited and can be overridden normally.
class Animal:
def speak(self):
return "Animal sound"
class Dog(Animal):
def speak(self): # Override
return "Woof!"
Class methods are also inherited and can be overridden. When a subclass calls a class method, cls refers to the subclass (not the parent).
class Base:
@classmethod
def create(cls, name):
return cls(name)
class Derived(Base):
pass
# Using the class method
obj1 = Base.create("base")
obj2 = Derived.create("derived")
print(type(obj1)) # <class '__main__.Base'>
print(type(obj2)) # <class '__main__.Derived'> (Derived.create calls Derived!)
This is why factory methods should use @classmethod β they respect inheritance automatically.
Static methods can also be overridden, but there is no special behavior (no cls or self).
class Calculator:
@staticmethod
def add(a, b):
return a + b
class AdvancedCalculator(Calculator):
@staticmethod
def add(a, b):
return a + b + 1 # Override
print(AdvancedCalculator.add(2, 3)) # 6
| Pitfall | Explanation | How to Avoid |
|---|---|---|
| Forgetting the decorator | def class_method(cls): without @classmethod makes it an instance method (where cls is actually self). |
Always use @classmethod and @staticmethod. |
Using self in a class method |
@classmethod def method(self): β self is not a convention here; it should be cls. |
Use cls for class methods, self for instance methods. |
| Hardcoding the class name in a factory method | return Person(name, age) inside a class method breaks inheritance. |
Use return cls(name, age) instead. |
Accessing class attributes with self |
In an instance method, self.attribute will look for an instance attribute first. |
Use self.__class__.attribute or ClassName.attribute for clarity. |
| Using a static method when a class method is needed | If you need to create instances or access class data, use a class method. | Think about what the method needs to access. |
Using @staticmethod for no reason |
Some functions are better as standalone functions; not everything needs to be in a class. | Keep static methods only when they are logically tied to the class. |
Date Class with Multiple ConstructorsLet's build a realistic Date class that demonstrates all three method types.
class Date:
"""A simple date class with multiple constructors."""
# Class attribute
MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
def __init__(self, year, month, day):
"""Instance method: initializer."""
self.year = year
self.month = month
self.day = day
# Instance method
def display(self):
"""Format the date as YYYY-MM-DD."""
return f"{self.year}-{self.month:02d}-{self.day:02d}"
# Instance method
def display_formatted(self):
"""Format the date as 12-Dec-2024."""
return f"{self.day} {Date.MONTHS[self.month-1]} {self.year}"
# Class method: factory from string
@classmethod
def from_string(cls, date_string, format_type="iso"):
"""Create a Date from various string formats."""
if format_type == "iso":
# "2024-12-25"
parts = date_string.split('-')
return cls(int(parts[0]), int(parts[1]), int(parts[2]))
elif format_type == "us":
# "12/25/2024"
parts = date_string.split('/')
return cls(int(parts[2]), int(parts[0]), int(parts[1]))
elif format_type == "named":
# "25 Dec 2024"
parts = date_string.split()
month_index = Date.MONTHS.index(parts[1]) + 1
return cls(int(parts[2]), month_index, int(parts[0]))
# Class method: today's date (simplified)
@classmethod
def today(cls):
"""Create a Date for today (simulated)."""
# In a real implementation, use datetime module
return cls(2024, 12, 25)
# Static method: validation
@staticmethod
def is_valid_date(year, month, day):
"""Check if a date is valid (simplified)."""
if month < 1 or month > 12:
return False
if day < 1 or day > 31:
return False
# Simplified: allow all months up to 31 days
return True
# --- Using the class ---
# 1. Normal construction
d1 = Date(2024, 12, 25)
print(d1.display()) # 2024-12-25
print(d1.display_formatted()) # 25 Dec 2024
# 2. Factory method from ISO string
d2 = Date.from_string("2024-10-31", "iso")
print(d2.display_formatted()) # 31 Oct 2024
# 3. Factory method from US format
d3 = Date.from_string("12/25/2024", "us")
print(d3.display_formatted()) # 25 Dec 2024
# 4. Factory method from named format
d4 = Date.from_string("25 Dec 2024", "named")
print(d4.display_formatted()) # 25 Dec 2024
# 5. Today's date
d_today = Date.today()
print(d_today.display()) # 2024-12-25
# 6. Static validation method
print(Date.is_valid_date(2024, 13, 1)) # False
print(Date.is_valid_date(2024, 12, 31)) # True
Key Observations:
__init__ is an instance method that initializes the object.display() and display_formatted() are instance methods that work with instance data.from_string() is a class method that provides alternative constructors.today() is a class method that creates a specific instance.is_valid_date() is a static method that performs validation without needing class or instance data.Answer the following questions to check your understanding.
1. What is the correct decorator for a class method in Python?
@staticmethod@classmethod@instancemethod2. What is the first parameter of a class method conventionally named?
selfclsclassinstance3. Which type of method cannot access instance attributes without being passed an instance?
4. Consider the following code:
class MyClass:
count = 0
@classmethod
def increment(cls):
cls.count += 1
@staticmethod
def is_positive(value):
return value > 0
Which method(s) can modify the count class attribute?
increment() onlyis_positive() only5. What is a common use case for a class method?
6. (True/False) A static method can access class attributes directly without any special syntax.
7. Why is it better to use cls rather than the class name in a class method?
8. Which method type would you use for a helper function that validates email addresses, and is logically related to a User class but doesn't need access to instance data?
9. Consider this code:
class Parent:
@classmethod
def create(cls, name):
return cls(name)
class Child(Parent):
def __init__(self, name):
self.name = name
c = Child.create("Bob")
print(type(c))
What is the output?
<class '__main__.Parent'><class '__main__.Child'><class 'object'>10. What will the following code print?
class Test:
@staticmethod
def hello():
return "Hello!"
t = Test()
print(t.hello())
Hello!TypeError because static methods can't be called on instancesHello! is printed but with a warning<bound method Test.hello of <class '__main__.Test'>>(Exercise text unchanged)
Part C: Reflection
After completing the exercise, answer the following in a comment:
Why did you choose to implement from_fahrenheit and from_kelvin as class methods rather than static methods?
(Homework text unchanged)
Part 3: Reflection Questions
Answer these in a comment block at the top of your script:
from_area_and_aspect_ratio a class method rather than an instance method?validate_color a static method rather than a class method or instance method?Shape instead of cls in the from_area_and_aspect_ratio method? Why is cls better?add_color method modifies a class attribute (available_colors). Could it have been a static method instead? Why or why not?display() on each shape in the list? Explain using the concepts from Tutorial 1.Bonus Challenge (Optional):
Add a calculate_perimeter() method to each shape. Add a class method calculate_total_area(shapes) to Shape that takes a list of shapes and returns the total area of all of them (hint: use a loop and polymorphism).
Before moving to Tutorial 8 (Dunder Methods), ensure you can confidently say YES to the following:
Ready for the next tutorial? In Tutorial 8, you will explore dunder (magic) methods β how to make your classes behave like Python built-ins by implementing __str__, __repr__, __len__, __eq__, and more.