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. 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