Previous | Tutorial index | Next

πŸ“˜ TUTORIAL 7: CLASS METHODS AND STATIC METHODS

Learning Objective

Understand the three types of methods and when to use each.

7.1 The Three Types of Methods in Python Classes

In Python, you can define three distinct types of methods within a class:

  1. Instance Methods – operate on individual instances; take self as first parameter.
  2. Class Methods – operate on the class itself; take cls as first parameter; use the @classmethod decorator.
  3. Static Methods – utility functions that belong to the class logically but don't access instance or class data; use the @staticmethod decorator; take no special first parameter.

Quick Comparison

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.

7.2 Instance Methods (The Default)

Characteristics

When to Use

Example

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.

7.3 Class Methods (@classmethod)

Characteristics

When to Use

  1. Factory Methods – creating instances in alternative ways.
  2. Class-level operations – modifying class attributes that affect all instances.
  3. Alternative constructors – providing multiple ways to instantiate a class.
  4. Inheritance-aware methods – methods that should respect subclass overrides.

Example: Factory Method

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.

Example: Class-Level Operations

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

7.4 Static Methods (@staticmethod)

Characteristics

When to Use

  1. Utility functions that are closely related to the class but don't need access to instance or class data.
  2. Helper functions that perform operations related to the class's domain.
  3. Validation functions that check input formats relevant to the class.
  4. Operations that could be standalone functions but belong logically with the class (for better code organization).

Example: Utility Methods

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

Example: Validation in a Class

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.

7.5 When to Use Each Method Type – A Decision Guide

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.

7.6 Inheritance and Method Types

Instance Methods in Inheritance

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 in Inheritance

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 in Inheritance

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

7.7 Common Pitfalls with Class and Static Methods

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.

7.8 Full Walkthrough Example: A Date Class with Multiple Constructors

Let'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:

πŸ“ Quiz 7: Class Methods and Static Methods

Answer the following questions to check your understanding.

1. What is the correct decorator for a class method in Python?

Answer(B) `@classmethod`

2. What is the first parameter of a class method conventionally named?

Answer(B) `cls`

3. Which type of method cannot access instance attributes without being passed an instance?

Answer(C) Static method

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?

Answer(A) `increment()` only

5. What is a common use case for a class method?

Answer(B) Factory methods (alternative constructors)

6. (True/False) A static method can access class attributes directly without any special syntax.

Answer(B) False – they need to use `Class.attribute` or be passed the class.

7. Why is it better to use cls rather than the class name in a class method?

Answer(B) It ensures inheritance works correctly.

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?

Answer(C) Static method

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?

Answer(B) ``

10. What will the following code print?

class Test: @staticmethod def hello(): return "Hello!" t = Test() print(t.hello())
Answer(A) `Hello!`

πŸ§ͺ Exercise 7: Building a System with All Method Types

(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?

Sample Answer `from_fahrenheit` and `from_kelvin` need to create new instances of the `Temperature` class, so they must be class methods because they receive `cls` and call `cls(celsius)` to construct the object. A static method does not receive `cls` and would have to hardcode the class name, which breaks inheritance. Using `cls` makes the factory methods flexible and subclass‑aware.

🏠 Homework 7: Building a Geometry Shape Factory System

(Homework text unchanged)

Part 3: Reflection Questions
Answer these in a comment block at the top of your script:

  1. Why did we make from_area_and_aspect_ratio a class method rather than an instance method?
  2. Why did we make validate_color a static method rather than a class method or instance method?
  3. What would happen if we used Shape instead of cls in the from_area_and_aspect_ratio method? Why is cls better?
  4. The add_color method modifies a class attribute (available_colors). Could it have been a static method instead? Why or why not?
  5. How does polymorphism work in the testing script when you call display() on each shape in the list? Explain using the concepts from Tutorial 1.
Sample Answers 1. `from_area_and_aspect_ratio` creates a new instance of the shape class. It must be a class method because it receives `cls` and returns `cls(...)`, ensuring that if a subclass overrides it, the subclass is used. 2. `validate_color` does not need access to any instance or class data; it only checks membership in a list. It is logically tied to the `Shape` class, so a static method keeps it organised without needing `self` or `cls`. 3. Using `Shape` would hardcode the parent class, so subclasses (e.g., `Rectangle`) would still return a `Shape` instance, breaking inheritance. `cls` ensures the correct subclass is instantiated. 4. It could be a static method if we accessed `Shape.available_colors` directly, but a class method is better because it uses `cls` and will work with subclasses if they override the attribute. Also, class methods clearly indicate that the operation is class‑level. 5. When we call `display()` on each shape in a list, the correct version of `display()` (from `Rectangle` or `Circle`) is executed automatically based on the actual object type. This is polymorphism: the same method name behaves differently depending on the object's class, thanks to inheritance and method overriding.

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).

πŸ“š Additional Resources for Self-Study

  1. Real Python: Python's Instance, Class, and Static Methods Demystified – Excellent deep dive with clear examples.
  2. CodeGym: staticmethod vs classmethod vs instance method – Good comparison.
  3. Programiz: Python @classmethod and @staticmethod – Concise explanation.
  4. Python Official Docs: 9. Classes – Method Objects – The authoritative source.

βœ… Summary Checklist for Tutorial 7

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.

Previous | Tutorial index | Next