Static & Class Methods in Python Classes

by Anonymous | November 25, 2020

 

Class methods and static methods are the topic of this article, see the example below which shows the syntax for calling these methods. These methods are useful for being explicit about what the functionality is intended to do. There are also strong arguments relating to maintainability. 

 

class Example:
    def __init__(self):
        pass
    
    @staticmethod
    def static_example():
        print("static method called")
    
    @classmethod
    def class_method(cls):
        print(f"class method called from {cls}")
    
    def instance_method(self):
        print(f"instance method called from {self}")




ex = Example()

ex.static_example()
ex.class_method()
ex.instance_method()

 

Notice below that the class method is called from the Example class itself. Whereas the instance method is called from an instance of the class.

 

static method called
class method called from <class '__main__.Example'>
instance method called from <__main__.Example object at 0x000001DD75320E48>

 

 

First argument passed for each method:

Class Method : Class usually denoted with cls

Static Method: Same as the first argument passed in a normal function, or None if defined that way.

Instance Method: The instance is passed as the first argument. 

 

 

Common Errors

Error 1

Instance attributes are not accessible through a static method. See the example below:

 

class Example:
    def __init__(self, name):
        self.name = name
    
    @staticmethod
    def static_example():
        print(self.name)
    
    @classmethod
    def class_method(cls):
        print(f"class method called from {cls}")
    
    def instance_method(self):
        print(f"instance method called from {self}")


ex = Example('Static Error')

ex.static_example()

 

Running the code above will result in a NameError, this is because static methods do not have access to the instance attributes. 

NameError: name 'self' is not defined

 

Therefore we should not use static methods if we are defining functions that need access to the variables within an instance / class. 

 

 

Error 2

This common error is very similar to the first. Notice below that we are trying to call self.name from a class method. This will result in a NameError as the class method won't have access to instance attributes (name in this case)

 

class Example:
    def __init__(self, name):
        self.name = name
    
    @staticmethod
    def static_example():
        print("static method called")
    
    @classmethod
    def class_method(cls):
        print(f"{self.name}")
    
    def instance_method(self):
        print(f"instance method called from {self}")



ex = Example('Class Error')

ex.class_method()

 

As expected running the code block above results in a NameError. This is because class methods do not have access to any instance variables. 

NameError: name 'self' is not defined

 

When creating class methods we should ensure that the method doesn't require anything related to an individual instance. 

 

 

When Should we use Static Methods? With real example. 

 

Static methods are useful for creating utility functions for things that are common to all instances of a class. Take the following example class called Product which is an example of an online shop. The class takes the product name and a price. 

 

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

 

Let's add a static method that will take a name argument and ensure it is of type: string. If The name is not a string then the static method should raise a TypeError. This is an example of a utility function. Notice below that when we are calling a static method we need to put the class name first and then the name of the method.

 

class Product:
    def __init__(self, name, price):
        self.name = Product.valid_name(name)
        self.price = price
    
    @staticmethod
    def valid_name(name):
        if isinstance(name, str):
            return name
        else:
            raise TypeError('Name must be a string')



p = Product(19.44, 'Hat')     
    

 

As expected the static method will help us catch errors. The results from trying to set p will be:

'TypeError: Name must be a string'

 

Let's add another two static methods below. One to validate the price variable and the other to set a variable relating to the time an instance of the Product class was created. The creation time static method defined below is prefixed with an _ . This indicates the method is not intended to be called from an instance after the object is initialized. This can be viewed as a private method.

 

import datetime as dt

class Product:
    def __init__(self, name, price):
        self.name = Product.valid_name(name)
        self.price = Product.valid_price(price)
        self._time_created = Product._creation_time()
    
    @staticmethod
    def valid_name(name):
        if isinstance(name, str):
            return name
        else:
            raise TypeError('Name must be a string')
            
    @staticmethod
    def valid_price(price):
        if not isinstance(price, float):
            raise TypeError('Price must be a float')
        if price <= 0:
            raise AttributeError('Price must be greater than 0')
        
        return price
    
    @staticmethod
    def _creation_time():
        return dt.datetime.now().isoformat() 
    
    @property
    def creation_time(self):
        return self._time_created
            


       
p = Product('Hat', 9.99)     

p2 = Product('TV', -3.00)
#AttributeError: Price must be greater than 0

p3 = Product('Game', '')
#TypeError: Price must be a float

p.creation_time
# '2020-11-24T14:33:09.021567'

 

So to summarize, we should use static methods as utility functions to perform functionality within an instance. Static methods can not be called using the self syntax. 

 

 

 

When Should we use Class Methods? With real example. 

 

We should use class methods when the functionality we are intending to implement requires access to class attributes. The example below named TimeConverter has three class attributes which are the number of minutes in an hour, number of hours in a day and number of days in a year. We will implement 3 class methods which convert minutes to hours, hours to days and days to years. Notice that the class methods pass in cls as the first argument. This allows the method to access the class data attributes defined at the beginning of the class scope.

 

class TimeConverter:
    mins_in_hour = 60
    hours_in_day = 24
    days_in_year = 365
    
    
    @classmethod
    def mins_to_hours(cls, mins):
        hours= mins // cls.mins_in_hour + \
                (mins % cls.mins_in_hour) / cls.mins_in_hour
        return hours
    
    @classmethod
    def hours_to_days(cls, hours):
        days = hours // cls.hours_in_day + \
                        (hours % cls.hours_in_day) / cls.hours_in_day
        return days
    
    @classmethod
    def days_to_years(cls, days):
        years = days // cls.days_in_year + \
                (days % cls.days_in_year) / cls.days_in_year
        return years



TimeConverter.mins_to_hours(300)
# returns : 5
TimeConverter.mins_to_hours(90)
# returns: 1.5
TimeConverter.mins_to_hours(120)     
 #returns 2.0      
TimeConverter.hours_to_days(24)
# returns 1.0
TimeConverter.hours_to_days(72)
# returns 3.0      
TimeConverter.hours_to_days(84)
# returns 3.5
TimeConverter.days_to_years(365)
# returns 1.0
TimeConverter.days_to_years(730)
#returns 2.0
TimeConverter.days_to_years(450)
#returns  1.2328767123287672

TimeConverter.days_to_years('string')
# TypeError: unsupported operand type(s) for //: 'str' and 'int'

 

The class works as expected for real numbers however, we don't do any type checking before the computation. So to finish off we can add a static method utility function to the class to check that the time we pass to the class methods is a nonnegative real number. 

 

from numbers import Real

class TimeConverter:
    mins_in_hour = 60
    hours_in_day = 24
    days_in_year = 365
    
    
    @classmethod
    def mins_to_hours(cls, mins):
        TimeConverter.valid_time(mins)
        hours= mins // cls.mins_in_hour + \
                (mins % cls.mins_in_hour) / cls.mins_in_hour
        return hours
    
    @classmethod
    def hours_to_days(cls, hours):
        TimeConverter.valid_time(hours)
        days = hours // cls.hours_in_day + \
                        (hours % cls.hours_in_day) / cls.hours_in_day
        return days
    
    @classmethod
    def days_to_years(cls, days):
        TimeConverter.valid_time(days)
        years = days // cls.days_in_year + \
                (days % cls.days_in_year) / cls.days_in_year
        return years
    
    
    @staticmethod
    def valid_time(time):
        if isinstance(time, Real) and time>=0:
            pass
        else:
            raise ValueError('Time must be a nonnegative real number')



TimeConverter.days_to_years('string')
# ValueError: Time must be a nonnegative real number  

 

 

Summary 

- To create class and static methods we can use the decorator syntax @staticmethod and @classmethod

 

- For class methods the first argument passed to the method is the class usually denoted cls. With an instance method the first argument is the instance usually denoted self. 

 

- Class methods have no access to instance data attributes defined in the constructor. Static methods do not have access to class or instance attributes. 

 

- Static methods can be viewed as utility functions to complete a task within the instance. They should be called using the ClassName.StaticMethodName syntax

 

- Class methods have access to only class attributes and should be called with the ClassName.ClassMethodName syntax

 

 


Join the discussion

Share this post with your friends!