staticmethod vs classmethod vs Instance Method in Python
6 min readBytePatterns
staticmethod vs classmethod vs instance method: the first argument each one gets, why alternative constructors use cls, and when a plain function wins.
staticmethod vs classmethod vs instance method in one line: they differ only in what Python passes as the first argument. An instance method receives the object (self), a class method receives the class it was called on (cls), and a static method receives nothing extra; it is a plain function that lives in the class's namespace. That one difference decides what each can reach: the instance's state, the class and its subclasses, or neither. The behaviour below follows the Python documentation, as of October 2026, and runs on Python 3.9, where one detail differs from newer versions and is pointed out.
| Kind | First argument | Typical job |
|---|---|---|
| instance method | self, the object | behaviour that reads or changes one object's state |
@classmethod | cls, the class it was called on | alternative constructors: from_string, from_dict |
@staticmethod | nothing | a helper that belongs with the class but needs neither |
The problem it solves
A class collects code around one idea, and not all of that code needs the same thing. Bumping a version needs a version. Parsing "1.4.2" needs no version yet, but it does need to know which class to build. Checking whether a string looks like a version needs neither. Writing all three as instance methods forces callers to build a throwaway object first; writing them all as module functions scatters the idea. The three method kinds let each function ask for exactly what it uses.
The intuition
Methods are functions stored on the class, and the decorators change what happens when you look one up.
- Instance method. A plain
defin a class. Looking it up on an instance,v.bump_minor, produces a bound method withvfilled in as the first argument. Looking it up on the class,Version.bump_minor, gives the plain function, so you would pass the instance yourself. - Class method.
@classmethodbinds the class instead. The documentation is precise: it can be called on the class or on an instance, the instance is ignored except for its class, and if it is called on a derived class, the derived class is passed. That last clause is the reason alternative constructors are class methods:cls(...)builds whatever subclass the caller asked for. - Static method.
@staticmethodturns binding off. The function gets exactly the arguments you pass. It can still be called asself.is_valid(...)orcls.is_valid(...), which is how a subclass can override it.
All three are descriptors. A function's __get__ returns a bound method; classmethod and staticmethod are wrapper objects whose __get__ returns the function bound to the class, or the bare function. You can see those wrapper objects in the class's __dict__. On Python 3.9 the raw staticmethod object is not callable by itself; Python 3.10 made it callable. Calling through the class or an instance works the same in every version.
When to use which
- It reads or writes this object's attributes: instance method.
- It builds an instance from another format, or needs class-level data that subclasses may override:
@classmethod, and construct withcls(...), never the class name. - It is a pure helper tied to the class's concept, maybe overridable:
@staticmethod. - It is a pure helper that nobody would look for on the class: a module-level function. Python modules are namespaces too, and a function there is easier to test and import.
Watch it run
The lesson's animation is a car hire desk. It opens on scattered Van() calls that hard-wire every caller to a concrete class, then shows a factory taking a key and handing back an object with the shape you asked for, with the registry as the one place that knows which key means which class. The key is looked up, one row matches, that class is built, and you get seats and a boot back without touching a constructor; an unknown key, "tank", fails in that same single place. When the depot retires the compact or adds a hybrid, one registry line changes and no caller does. A class method like from_string is that factory attached to the class itself: callers ask for "a version from this text", and cls decides which class gets built.
Factory Pattern
Step 1 of 11
Scattered Van() calls hard-wire every caller to a concrete class.
The same interactive animation as the lesson — step through it with the controls.
The code
A Version class with all three kinds, and a look at what each lookup is bound to:
class Version:
def __init__(self, major, minor, patch):
self.major, self.minor, self.patch = major, minor, patch
def bump_minor(self): # instance method: gets the object
return type(self)(self.major, self.minor + 1, 0)
@classmethod
def from_string(cls, text): # class method: gets the class
if not cls.is_valid(text):
raise ValueError("not a version: " + repr(text))
return cls(*map(int, text.split(".")))
@classmethod
def from_dict(cls, d):
return cls(d["major"], d["minor"], d["patch"])
@staticmethod
def is_valid(text): # static method: gets nothing extra
parts = text.split(".")
return len(parts) == 3 and all(p.isdigit() for p in parts)
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"
def __eq__(self, other):
return type(self) is type(other) and str(self) == str(other)
v = Version.from_string("1.4.2")
print(v, v.bump_minor(), Version.is_valid("1.4")) # 1.4.2 1.5.0 False
print(Version.bump_minor(v), v.from_string("2.0.0")) # 1.5.0 2.0.0 <- both spellings work
print(v.bump_minor.__self__ is v) # True <- bound to the instance
print(Version.from_string.__self__ is Version) # True <- bound to the class
print(Version.is_valid is v.is_valid) # True <- a plain function, no binding
try:
Version.bump_minor()
except TypeError as e:
print(e) # bump_minor() missing 1 required positional argument: 'self'
Now a subclass. The class methods build the subclass without a line of change, and so does bump_minor, because it uses type(self). A static "constructor" that names Version loses the subclass. Then the descriptors themselves, straight from __dict__:
class LtsVersion(Version):
support_years = 3
@staticmethod
def parse_static(text): # a "constructor" that names its class
return Version(*map(int, text.split(".")))
lts = LtsVersion.from_string("2.0.0")
print(type(lts).__name__, lts.support_years) # LtsVersion 3
print(type(lts.bump_minor()).__name__) # LtsVersion
print(type(LtsVersion.from_dict({"major": 2, "minor": 1, "patch": 0})).__name__) # LtsVersion
print(type(LtsVersion.parse_static("2.0.0")).__name__) # Version <- the subclass is lost
raw = Version.__dict__ # what the decorators stored
print(type(raw["bump_minor"]).__name__, type(raw["from_string"]).__name__,
type(raw["is_valid"]).__name__) # function classmethod staticmethod
print(raw["from_string"].__get__(None, LtsVersion)("3.1.0").__class__.__name__) # LtsVersion
try:
raw["is_valid"]("1.2.3") # the raw object, not via the class
except TypeError as e:
print(e) # 'staticmethod' object is not callable
The __get__ line is what LtsVersion.from_string does behind the scenes: the descriptor binds to the class it was reached through. The last line is the Python 3.9 detail; on 3.10 and later it returns True. Finally, a seeded round trip: for 5,000 random versions of both classes, parsing the printed form and rebuilding from a dict must give back an equal object of the same class, and the validator must accept only real versions:
import random
def to_dict(v):
return {"major": v.major, "minor": v.minor, "patch": v.patch}
rng = random.Random(11)
ok = True
for _ in range(5000):
cls = rng.choice([Version, LtsVersion])
x = cls(rng.randint(0, 99), rng.randint(0, 99), rng.randint(0, 99))
ok &= cls.from_string(str(x)) == x # string round trip
ok &= cls.from_dict(to_dict(x)) == x # dict round trip
ok &= type(x.bump_minor()) is cls # the class survives
junk = rng.choice(["1.2", "1.2.x", "", "1..2", "v1.2.3", str(x)])
ok &= Version.is_valid(junk) == (junk == str(x))
print(ok) # True
try:
Version.from_string("1.2")
except ValueError as e:
print(e) # not a version: '1.2'
__eq__ compares the exact type, so the round trip would fail if any constructor silently returned a Version for an LtsVersion.
The complexity
The call cost is the same order for all three: one attribute lookup, one descriptor __get__, one call. Every obj.method lookup goes through the descriptor, which only matters in very tight loops, where hoisting m = obj.method out of the loop is the usual fix. The real costs are design costs:
- Instance method that ignores
self: callers must construct an object they do not need. - Static method that builds instances: every subclass must override it.
- Class method that names the class: same problem, written with more ceremony.
Where it goes wrong
return Version(...)inside a class method. It works until someone subclasses. Usecls(...).- Making everything static. A class full of static methods is a module in disguise; a module is simpler.
- Mutating class state from a class method by accident.
cls.count += 1called on a subclass creates or changes the subclass's attribute, not the base class's. - Forgetting
self.def bump_minor():fails only when called, with a "takes 0 positional arguments but 1 was given" error. - Relying on a raw
staticmethodbeing callable. Code that calls one stored in a dict or from__dict__breaks on 3.9 and earlier.
When it shows up in interviews
In Python screening rounds as the literal question, and in low-level design rounds whenever you write a constructor that parses input: "how would you build an Order from a JSON payload?" is an invitation to write from_dict as a class method. The pattern it serves is in the factory pattern explained, and when a subclass is the wrong tool in the first place, composition vs inheritance is the follow-up. The registry-based factory itself is the Factory Pattern lesson.
How to say it in an interview
"The difference is the first argument. An instance method gets self, so it works on one object's state. A class method gets cls, the class it was called on, even when that is a subclass, which is why alternative constructors like from_string or from_dict are class methods that return cls(...). A static method gets nothing; it is a function namespaced on the class, useful for a helper like a validator that subclasses might override. Under the hood they are descriptors that bind differently. If a helper has nothing to do with the class, I make it a module function."
Frequently asked questions
What is the difference between staticmethod and classmethod in Python?
A class method receives the class it was called on as its first argument, so it can construct instances and respect subclasses. A static method receives no implicit argument at all; it is a regular function stored on the class. Use a class method when the class matters, a static method when it does not.
When should I use a classmethod in Python?
Mainly for alternative constructors, such as from_string, from_dict or from_json, that parse input and return cls(...). Because cls is whatever class the method was called on, a subclass calling the inherited constructor gets an instance of itself. Class methods also suit operations on class-level configuration.
Should I use a static method or a module-level function?
Prefer a module-level function unless the helper clearly belongs to the class's concept, is used mostly by its methods, or should be overridable by subclasses. A static method adds a namespace and nothing else, and a module already is a namespace.
Sources
- Built-in Functions: classmethod and staticmethod — Python Documentation
- Descriptor HowTo Guide: functions and methods, static and class methods — Python Documentation
- Data model: instance methods and
__get__— Python Documentation - Classes tutorial: method objects — Python Documentation