Skip to content
BytePatterns

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.

KindFirst argumentTypical job
instance methodself, the objectbehaviour that reads or changes one object's state
@classmethodcls, the class it was called onalternative constructors: from_string, from_dict
@staticmethodnothinga 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 def in a class. Looking it up on an instance, v.bump_minor, produces a bound method with v filled 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. @classmethod binds 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. @staticmethod turns binding off. The function gets exactly the arguments you pass. It can still be called as self.is_valid(...) or cls.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 with cls(...), 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. Use cls(...).
  • 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 += 1 called 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 staticmethod being 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