Julius Seporaitis
on hobbies and work

Thoughts On @staticmethod Usage In Python

At least one popular Python style-guide, Google Python Styleguide, insists1 on not-using @staticmethod decorator and suggests to use a module-level function instead. Guido van Rossum has called the static method a “somewhat mistake2. Still, neither of these sources provide much explanation in practical terms - why.

I have also been a pronounced critic of @staticmethod decorator in code reviews at work. However, my colleague recently asked why do I dislike static methods so much, so I thought I would put it down in writing. It’s not that I dislike them, but I consider them a code smell that leads to issues down the line.

One argument I heard for using @staticmethod is that because a method is a utility and does not need the class or instance to do its job. Or another - it’s a way to namespace the helper under the class which uses it. Both sound like the correct thing to do. However, when it comes to Python code - I disagree. Here are a few aspects to consider.

API Design

Any function, module, or package has an API that an engineer is responsible for defining. A good API clearly defines the operations that can be invoked and how to invoke them. Most OO programming languages have helpful language features to enforce that. Unfortunately, Python is not one of those languages, despite sharing some similar keywords. For instance, even when it claims to have private or protected methods, they are more of a convention than actual scope control.

It may seem obvious, but it comes at the cost of some familiar things (like static methods) contributing to counter-intuitive outcomes. Consider a simplified example:

class Foo:

    def run(self):
        # ...
        self.get_something(param1, param2)
        # ...

    @staticmethod
    def get_something(param1, param2):
        return param1 + param2

get_something is a helper method, and it does not need any instance variables to do its job - so @staticmethod makes this clear. However, what is lost here is the scope of this method. Is it supposed to be private or public? Private, in this context, means it should be called from a class instance. Let’s assume private and supposed to be used just in instances of Foo.

Everyone comes with personal predispositions about what’s acceptable and what’s not in code - this whole post is about mine. For someone, it may seem perfectly reasonable to import Foo and call Foo.get_something in some other package, module, or class, without asking or checking. Here lies an interesting disconnect between the author and the user of this method: Author of Foo abdicated the control of the method to anyone who finds it useful.

An engineer who uses this piece of code may not be aware of the original intent or scope of this method.

In other words: a rule in codebase not enforced is bound to be stretched at some point - it is easy to notice such a situation during code reviews in a team of 5. In a team of 10 or more, it is a reasonable assumption that, without any other scope control, a method that is not intended but is available for public use - will be used as such at least once.

The above proposition is better known as Hyrum’s Law3.

Hence, if there is a way to enforce that scope, it should be used. If the author does not intend a method to be called without instantiating the class or hopes to keep it for use within class instance - it should be an instance method (a.k.a. without the @staticmethod decorator). Regardless of whether it needs to access anything in the class or not. The obvious benefit is that the interpreter will enforce this rule automatically, and get_something will only be callable from Foo instances.

Maintenance

As business goals, priorities, and requirements change - so does the code. What was Foo one day, may need to become Bar another. It helps to think about this when writing code. With a @staticmethod it’s a question of how many search-and-replace operations in a codebase you (or someone else) will have to do.

Let’s assume now the opposite of above - Foo.get_something is indeed supposed to be a publicly callable method. It is now an obnoxious dependency to manage. Consider this piece of code:

from app.foo import Foo

class Bar:

    def do_something(self):
        # ...
        Foo.get_something(param1, param2)
        # ...

To call the static method any other module needs to import the class (from app.foo import Foo) and invoke Foo.get_something(). If get_something is indeed a utility, by its nature, it is unlikely to change much over the lifetime of the module. However, the code using it, Foo and Bar classes, is more than likely to evolve and change. Assuming this is a somewhat popular utility method, it can quickly become frustrating to refactor the code. Say having to change Foo to NewFoo will end up requiring changes in all sorts of unexpected places - collateral that takes time to resolve when it easily could be avoided.

Hence, if there is a way to save your time running a search and replace project-wide and having to update tests and other seemingly unrelated code, it should probably be used. In this case, a simple module-level function will do the work. The classes that use it can be changed as much as needed, and the utility and its other invocations can be left alone keeping a refactoring scoped to only what’s changed.

Pragmatism

From a purely pragmatic perspective of wanting to type less, having to type self.get_something or a SomeWhatDescriptiveClassName.get_something is just a waste of time and horizontal space in a language which relies heavily on indentation and where get_something would suffice. Python is good at namespacing things with packages and modules and regardless of the intent to be public or private - a module-level utility function will do the job.

Teamwork

There is a natural tendency to write and think of the code as something existing right now, written by you - the author. We make assumptions unconsciously, and there are many of them:

  • the editor and the environment the code will be changed in will be the same as now;
  • the author of the code will remain the same;
  • an auto-complete will always be present;
  • build tooling will always be present;
  • the author will still be able to review the code and catch any misusage; etc.

Some of these assumptions are natural, but the older the project - the less likely they are to stay true. Some of these things will erode, and the code with all these little inconsequential bits and pieces will become a nuisance to handle.

Hence, if some of these problems can be avoided early on, especially just by the force of style and habit, they should. I consider @staticmethod usage to be something from that area, and that’s why I raise it during code reviews.


[Footnotes]

  1. From Google Python Styleguide:

    Never use @staticmethod unless forced to in order to integrate with an API defined in an existing library. Write a module level function instead.

    ↩︎

  2. Guido van Rossum in python-ideas mailing list:

    Honestly, staticmethod was something of a mistake – I was trying to do something like Java class methods but once it was released I found what was really needed was classmethod. But it was too late to get rid of staticmethod.

    ↩︎

  3. Definition of Hyrum’s Law:

    • With a sufficient number of users of an API,
    • it does not matter what you promise in the contract:
    • all observable behaviors of your system
    • will be depended on by somebody.

    ↩︎