cd /news/developer-tools/when-to-use-classmethod-staticmethod… · home topics developer-tools article
[ARTICLE · art-32326] src=belderbos.dev ↗ pub= topic=developer-tools verified=true sentiment=· neutral

When to use classmethod, staticmethod, or instance method in Python

A Python developer explains the decision rule for choosing between instance methods, classmethods, and staticmethods, emphasizing that classmethods are best used as alternative constructors or for class-level state management, with examples from the standard library and Pydantic.

read6 min views1 publishedJun 18, 2026

Shipping fast with AI but don't fully trust the code? I help developers 1:1 turn AI-built apps into something they understand and own. How it works →

In a coaching call this week we discussed a create

classmethod, and someone asked the obvious question: why is that here? It just forwarded its arguments to __init__

. We ended up discussing the difference between instance methods, classmethods, and staticmethods, and how to tell which is which. Here's a simple decision rule.

The decision rule #

Look at what the method actually touches:

Needs the instance(self

) → instance methodNeeds the class(cls

) but not a specific instance →@classmethod

Needs neither@staticmethod

Nice, but what are some actual use cases? Let's look at the create

method that prompted the question.

The create

method from the call fails the rule above. It took the same arguments as __init__

and passed them straight through. It still adds a nice interface (Class.create(...)

), but it doesn't do any work that the constructor doesn't already do:

@classmethod
def create(cls, amount: Decimal, currency: Currency = Currency.EUR) -> "Expense":
    return cls(amount=amount, currency=currency)

When a classmethod earns its place #

A classmethod pulls its weight when it does work the constructor shouldn't, or builds the object from a different starting point. Add a normalization step and the same method suddenly has a job:

@classmethod
def create(cls, amount: Decimal, currency: Currency = Currency.EUR) -> "Expense":
    return cls(amount=amount.quantize(Decimal("0.01")), currency=currency)

The canonical use of a @classmethod

is the alternative constructor. Python won't let you overload __init__

, so when you want to build an object several ways, each way becomes a classmethod.

The standard library has rich examples, for example take a look at datetime.date

:

date.today()                      # from the system clock
date.fromtimestamp(1718539200)    # from a POSIX timestamp
date.fromisoformat("2026-06-16")  # from an ISO 8601 string
date.fromordinal(739418)          # from a proleptic Gregorian ordinal
date.fromisocalendar(2026, 25, 1) # from ISO year/week/day

Source:


    @classmethod
    def fromtimestamp(cls, t):
        "Construct a date from a POSIX timestamp (like time.time())."
        if t is None:
            raise TypeError("'NoneType' object cannot be interpreted as an integer")
        y, m, d, hh, mm, ss, weekday, jday, dst = _time.localtime(t)
        return cls(y, m, d)

    @classmethod
    def today(cls):
        "Construct a date from time.time()."
        t = _time.time()
        return cls.fromtimestamp(t)

    ...
    ...

Every one of those returns a date

, but starts from different raw material. They have to be classmethods because they need cls

to construct the instance, and they return cls(...)

which makes it also work with subclasses. For instance, if MyDate

subclasses date

, then MyDate.today()

will return a MyDate

instance, not a date

.

Bonus: I was annoyed that my pysource package didn't work, so I've since patched it, and now you can get to this source code easily with:

uvx --from pybites-pysource pysource -m datetime.date

(I tend to pip this into Vim with | vi -

to read the source code in a scratch buffer.)

You'll see the same pattern across the ecosystem: dict.fromkeys(...)

, int.from_bytes(...)

, and in Pydantic Model.model_validate(...) /

model_validate_json(...)

are all classmethods that build an instance from different raw material.Another classmethod use case is class-level state: registries, caches, counters. A plugin registry is the clean example, because the method reads and mutates state that belongs to the class, not to any instance:

class Handler:
    _registry: dict[str, type["Handler"]] = {}

    @classmethod
    def register(cls, name: str, handler: type["Handler"]) -> None:
        cls._registry[name] = handler

    @classmethod
    def get(cls, name: str) -> type["Handler"]:
        return cls._registry[name]

Handler.register("json", JSONHandler)

When it's really a staticmethod #

If the method touches neither self

nor cls

, it's a staticmethod, which is a plain function that happens to live inside the class for namespacing. That's a legitimate choice when the helper is tightly bound to the class and you want Expense.normalize(...)

to read well. It's now part of the class API (it shows up in dir(Expense)

) and can be called without an instance.

Genuine staticmethods are rarer than the other two, which itself tells you something. A clean example is a Color

class with conversion helpers (from a Pybites exercise):

class Color:
    def __init__(self, name: str):
        self.name = name
        self.rgb = COLOR_NAMES.get(name.upper())

    @staticmethod
    def hex2rgb(hex_value: str) -> tuple[int, int, int]:
        return tuple(int(hex_value[i:i + 2], 16) for i in (1, 3, 5))

    @staticmethod
    def rgb2hex(rgb: tuple[int, int, int]) -> str:
        return f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}"

hex2rgb

and rgb2hex

touch neither the instance nor the class. They're pure conversions that live on Color

so Color.hex2rgb("#ff0000")

reads well next to the rest of the API.

But that's exactly the signal worth noticing: a staticmethod might be just a function in disguise, and sometimes the honest move is to pull it out to a module-level function where it's easier to test and use on its own.

Summary #

Method type First argument Access to Common use case
Instance method self Instance & class state Modifying object state
Class method (@classmethod ) cls Class state only Alternative constructors, registries
Static method (@staticmethod ) none Neither Isolated utility/helper functions

Why this matters more now #

When you write the code yourself, you rarely add a method without a reason. When an agent writes it, you get plausible-looking structure that nobody chose. A create

classmethod that does nothing, a staticmethod that should be a free function, a helper hanging off the wrong class. That's your judgment call: is this method doing work that belongs to the class, or is it just a pattern the agent learned from other code?

It pays to slow down and look critically at any code and ask those questions. With AI producing more code faster, it's easy to assume that if it looks like Python, it's good Python. But the agent has no taste, and it will happily produce code that is technically correct but structurally wrong.

This is also why I keep writing articles like this one: to give you a simple decision rule you can run in your head during review. It reminds me of Rust, which makes data flow explicit right in the signature with self

, &self

, and &mut self

. The signature tells you what the method touches, same idea as the rule here. (That data-and-behavior split is the whole theme of Why Rust does not need OOP.)

So use AI, but keep developing your knowledge and taste. The more you know, the better you judge the code that comes your way, whether a human or an agent wrote it.

Shipping fast with AI is the easy part. Knowing what to keep, rewrite, and trust is the hard part. I work with developers 1:1 to audit AI-built codebases, trace the real control flow, and make them something you can explain and own, without leaning on a chatbot. How 1:1 coaching works →

── more in #developer-tools 4 stories · sorted by recency
── more on @python 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/when-to-use-classmet…] indexed:0 read:6min 2026-06-18 ·