{"slug": "when-to-use-classmethod-staticmethod-or-instance-method-in-python", "title": "When to use classmethod, staticmethod, or instance method in Python", "summary": "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.", "body_md": "# When to use classmethod, staticmethod, or instance method in Python\n\n*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 →*\n\nIn a coaching call this week we discussed a `create`\n\nclassmethod, and someone asked the obvious question: why is that here? It just forwarded its arguments to `__init__`\n\n. 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.\n\n## The decision rule\n\nLook at what the method actually touches:\n\n**Needs the instance**(`self`\n\n) → instance method**Needs the class**(`cls`\n\n) but not a specific instance →`@classmethod`\n\n**Needs neither**→`@staticmethod`\n\nNice, but what are some actual use cases? Let's look at the `create`\n\nmethod that prompted the question.\n\nThe `create`\n\nmethod from the call fails the rule above. It took the same arguments as `__init__`\n\nand passed them straight through. It still adds a nice interface (`Class.create(...)`\n\n), but it doesn't do any work that the constructor doesn't already do:\n\n``` python\n# shortened for clarity\n@classmethod\ndef create(cls, amount: Decimal, currency: Currency = Currency.EUR) -> \"Expense\":\n    return cls(amount=amount, currency=currency)\n```\n\n## When a classmethod earns its place\n\nA 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:\n\n``` python\n@classmethod\ndef create(cls, amount: Decimal, currency: Currency = Currency.EUR) -> \"Expense\":\n    return cls(amount=amount.quantize(Decimal(\"0.01\")), currency=currency)\n```\n\nThe canonical use of a `@classmethod`\n\nis the **alternative constructor**. Python won't let you overload `__init__`\n\n, so when you want to build an object several ways, each way becomes a classmethod.\n\nThe standard library has rich examples, for example take a look at `datetime.date`\n\n:\n\n```\ndate.today()                      # from the system clock\ndate.fromtimestamp(1718539200)    # from a POSIX timestamp\ndate.fromisoformat(\"2026-06-16\")  # from an ISO 8601 string\ndate.fromordinal(739418)          # from a proleptic Gregorian ordinal\ndate.fromisocalendar(2026, 25, 1) # from ISO year/week/day\n```\n\nSource:\n\n``` python\n    # Additional constructors\n\n    @classmethod\n    def fromtimestamp(cls, t):\n        \"Construct a date from a POSIX timestamp (like time.time()).\"\n        if t is None:\n            raise TypeError(\"'NoneType' object cannot be interpreted as an integer\")\n        y, m, d, hh, mm, ss, weekday, jday, dst = _time.localtime(t)\n        return cls(y, m, d)\n\n    @classmethod\n    def today(cls):\n        \"Construct a date from time.time().\"\n        t = _time.time()\n        return cls.fromtimestamp(t)\n\n    ...\n    ...\n```\n\nEvery one of those returns a `date`\n\n, but starts from different raw material. They have to be classmethods because they need `cls`\n\nto construct the instance, and they return `cls(...)`\n\nwhich makes it also work with subclasses. For instance, if `MyDate`\n\nsubclasses `date`\n\n, then `MyDate.today()`\n\nwill return a `MyDate`\n\ninstance, not a `date`\n\n.\n\n**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:\n\n```\nuvx --from pybites-pysource pysource -m datetime.date\n```\n\n(I tend to pip this into Vim with `| vi -`\n\nto read the source code in a scratch buffer.)\n\nYou'll see the same pattern across the ecosystem: `dict.fromkeys(...)`\n\n, `int.from_bytes(...)`\n\n, and in Pydantic [ Model.model_validate(...)](https://grep.app/pydantic/pydantic/main/pydantic/main.py?q=model_validate#L722) /\n\n`model_validate_json(...)`\n\nare 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:\n\n```\nclass Handler:\n    _registry: dict[str, type[\"Handler\"]] = {}\n\n    @classmethod\n    def register(cls, name: str, handler: type[\"Handler\"]) -> None:\n        cls._registry[name] = handler\n\n    @classmethod\n    def get(cls, name: str) -> type[\"Handler\"]:\n        return cls._registry[name]\n\n# called on the class, no instance needed; it mutates state that lives on the class\nHandler.register(\"json\", JSONHandler)\n```\n\n## When it's really a staticmethod\n\nIf the method touches neither `self`\n\nnor `cls`\n\n, 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(...)`\n\nto read well. It's now part of the class API (it shows up in `dir(Expense)`\n\n) and can be called without an instance.\n\nGenuine staticmethods are rarer than the other two, which itself tells you something. A clean example is a `Color`\n\nclass with conversion helpers (from a Pybites exercise):\n\n``` python\nclass Color:\n    def __init__(self, name: str):\n        self.name = name\n        self.rgb = COLOR_NAMES.get(name.upper())\n\n    @staticmethod\n    def hex2rgb(hex_value: str) -> tuple[int, int, int]:\n        return tuple(int(hex_value[i:i + 2], 16) for i in (1, 3, 5))\n\n    @staticmethod\n    def rgb2hex(rgb: tuple[int, int, int]) -> str:\n        return f\"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}\"\n```\n\n`hex2rgb`\n\nand `rgb2hex`\n\ntouch neither the instance nor the class. They're pure conversions that live on `Color`\n\nso `Color.hex2rgb(\"#ff0000\")`\n\nreads well next to the rest of the API.\n\nBut 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.\n\n## Summary\n\n| Method type | First argument | Access to | Common use case |\n|---|---|---|---|\n| Instance method | `self` | Instance & class state | Modifying object state |\nClass method (`@classmethod` ) | `cls` | Class state only | Alternative constructors, registries |\nStatic method (`@staticmethod` ) | none | Neither | Isolated utility/helper functions |\n\n## Why this matters more now\n\nWhen 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`\n\nclassmethod 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?\n\nIt 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.\n\nThis 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`\n\n, `&self`\n\n, and `&mut self`\n\n. 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/).)\n\nSo 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.\n\nShipping 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)", "url": "https://wpnews.pro/news/when-to-use-classmethod-staticmethod-or-instance-method-in-python", "canonical_source": "https://belderbos.dev/blog/classmethod-vs-staticmethod-vs-instance-method-python/", "published_at": "2026-06-18 00:00:00+00:00", "updated_at": "2026-06-18 09:01:45.654654+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Python", "Pydantic", "datetime.date", "dict.fromkeys", "int.from_bytes"], "alternates": {"html": "https://wpnews.pro/news/when-to-use-classmethod-staticmethod-or-instance-method-in-python", "markdown": "https://wpnews.pro/news/when-to-use-classmethod-staticmethod-or-instance-method-in-python.md", "text": "https://wpnews.pro/news/when-to-use-classmethod-staticmethod-or-instance-method-in-python.txt", "jsonld": "https://wpnews.pro/news/when-to-use-classmethod-staticmethod-or-instance-method-in-python.jsonld"}}