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

> Source: <https://belderbos.dev/blog/classmethod-vs-staticmethod-vs-instance-method-python/>
> Published: 2026-06-18 00:00:00+00:00

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

*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 method**Needs 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:

``` python
# shortened for clarity
@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:

``` python
@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:

``` python
    # Additional constructors

    @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](https://github.com/PyBites-Open-Source/pysource) 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(...)](https://grep.app/pydantic/pydantic/main/pydantic/main.py?q=model_validate#L722) /

`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]

# called on the class, no instance needed; it mutates state that lives on the class
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):

``` python
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](/blog/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 →](/coaching/#own-project)
