Skip to main content

Getting started with classes in Python Part 5: `classmethod` and `staticmethod`

james.derrick@ansys.com | 12.03.2024

Methods on classes have to include the self parameter to work properly, however, there are two notable exceptions to this rule that are worth learning about. They are classmethods and staticmethods.

staticmethod

A method is said to be "static" when it doesn't use the class properties at all. In other words, "self" is unused within the method body. However, you can't then remove the parameter self from the definition because it has to be there for methods to work. Unless, that is, you use the decorator staticmethod on the method. If that is applied you can remove the self parameter and still have other non-self parameters. Static methods like this can still be called using self within other methods, but they can also be called via the class and instances of the class. Why would you ever want to do this though? It crops up surprisingly often, but generally you may want to group utility functions together to make them easier to use.

For example, consider the Coordinate class from Part 4. A static method is generally a method or function that is strongly associated with the class, even if it doesn't use the class properties.

from dataclasses import dataclass


@dataclass(frozen=True)
class Coordinate:
    x: int
    y: int
    z: int

For Coordinate we might want to create a utility function to get possible values of x, y and z. Let's fix the minimum and maximum values of the three dimensions as constants at the beginning of the file. Now we can call them in the class. Our staticmethod would look something like this.

from dataclasses import dataclass


MIN_X, MAX_X = (0, 10)
MIN_Y, MAX_Y = (0, 10)
MIN_Z, MAX_Z = (0, 10)


@dataclass(frozen=True)
class Coordinate:
    x: int
    y: int
    z: int

    @staticmethod
    def get_possible_values(axis: str):
        axis = axis.lower()
        if axis == 'x':
            min_, max_ = MIN_X, MAX_X
        elif axis == 'y':
            min_, max_ = MIN_Y, MAX_Y
        elif axis == 'z':
            min_, max_ = MIN_Z, MAX_Z
        else:
            raise ValueError(f'axis {axis} is invalid. Choose "x", "y", or "z".') 
        return [i for i in range(min_, max_+1)]


# The staticmethod can be called from both the class and instances of the class!
print(Coordinate.get_possible_values('x'))
print(Coordinate(0, 0, 0).get_possible_values('x'))

Thus we can write a class method that does not use self. If you still aren't convinced by this tool's efficacy don't worry. Once you've created your own classes and developed your own software using them, you will come across situations like this and this is a tool for those times.

classmethod

Class methods are just as easy to create and apply as static methods, but they have a very different purpose. Class methods are instance creation methods. This is easier to explain with an example. To do so, let's consider the ThreeDimensionalSpace class from Part 4.

@dataclass
class ThreeDimensionalSpace:
    coordinates: set[Coordinate]

I have omitted the other methods as we don't need them for the example.

When working in 3D cartesian space (i.e. in some sort of 'box') we need to create instances of Coordinate for every point, but all we need to know to do this is really the minimum and maximum values for x, y, and z. Since x, y, and z must be integers we can work out every possible coordinate from just 6 numbers: min and max of x, y, and z.

However, the class ThreeDimensionalSpace needs us to create the set of all these coordinates every time before we can instantiate it. What if we built it in?

@dataclass
class ThreeDimensionalSpace:
    coordinates: set[Coordinate]

    @classmethod
    def from_min_max(cls, xlims: tuple[int, int], ylims: tuple[int, int], zlims: tuple[int, int]):
        coordinates = set()
        for x in range(xlims[0], xlims[1]+1):
            for y in range(ylims[0], ylims[1]+1):
                for z in range(zlims[0], zlims[1]+1):
                    coordinates.add(Coordinate(x, y, z))
        return cls(coordinates)

Enter the classmethod. This is a method that can be called from the class itself, or an instance of a class like staticmethod, and its selling point is that it creates a new instance of the class. The first parameter for a method decorated with classmethod must always be cls, which is the constructor of the class. In practice this lets you add class-creation shortcuts to your work. It can even be used as the only way to instantiate the class, which can be useful when you don't want users to construct the class themselves in the "normal" way or perhaps there is a private way of instantiating the class that you don't want users to see.

In [12]: new = ThreeDimensionalSpace.from_min_max((0, 2), (0, 2), (0, 2))

In [13]: for c in new.coordinates:
    ...:     print(c)
    ...:
Coordinate(x=2, y=0, z=2)
Coordinate(x=0, y=1, z=0)
Coordinate(x=2, y=2, z=2)
Coordinate(x=2, y=1, z=0)
Coordinate(x=1, y=2, z=2)
Coordinate(x=0, y=0, z=1)
Coordinate(x=0, y=2, z=1)
Coordinate(x=1, y=0, z=1)
Coordinate(x=1, y=1, z=0)
Coordinate(x=2, y=0, z=1)
Coordinate(x=2, y=1, z=2)
Coordinate(x=2, y=2, z=1)
Coordinate(x=0, y=1, z=2)
Coordinate(x=1, y=2, z=1)
Coordinate(x=0, y=2, z=0)
Coordinate(x=0, y=0, z=0)
Coordinate(x=1, y=1, z=2)
Coordinate(x=1, y=0, z=0)
Coordinate(x=2, y=0, z=0)
Coordinate(x=2, y=2, z=0)
Coordinate(x=0, y=1, z=1)
Coordinate(x=2, y=1, z=1)
Coordinate(x=1, y=2, z=0)
Coordinate(x=0, y=0, z=2)
Coordinate(x=0, y=2, z=2)
Coordinate(x=1, y=0, z=2)
Coordinate(x=1, y=1, z=1)

Hopefully the utility of class methods should be self-evident. However both of these decorators are simply useful tools that you should know about. In this part I have covered static and class methods, and I briefly mentioned the concept of "Private" methods and classes, which will be the subject of the next installment of this series. Stay tuned for Part 6: Public, Private, and Super Private Naming.

For a complete look at the articles in this series, have a look at this overview page: Getting started with classes in Python