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
mistake”2.
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]
-
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. -
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.
-
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.