Check this post on my blog here.
If you've ever found yourself staring at @classmethod and @staticmethod wondering, “Wait, when do I use which again?”, you're not alone. I’ve been there too. These method types are simple on the surface but hide a lot of subtle power that can make your code cleaner, more maintainable, and easier to test.
Let’s walk through them one step at a time starting with a shared base class to keep things practical.
We'll use a base class called UserAccount. Imagine this is part of a backend for a SaaS application managing users.
class UserAccount:
platform = "MySaaSPlatform"
def __init__(self, username, email):
self.username = username
self.email = emailSo far, this is a classic class with an __init__ method. Let’s explore how each method type behaves by adding features step by step.
Instance methods are what you normally write. They access and modify the specific instance of the class. The first argument is always self.
When your method needs to read or write instance-specific data like user email or username, use this.
Let’s add one:
class UserAccount:
platform = "MySaaSPlatform"
def __init__(self, username, email):
self.username = username
self.email = email
def get_profile(self):
return {
"username": self.username,
"email": self.email,
"platform": self.platform
}Now we can use it like below.
user = UserAccount("alice", "alice@example.com")
print(user.get_profile())
# {'username': 'alice', 'email': 'alice@example.com', 'platform': 'MySaaSPlatform'}So, what's happening here?
self.usernameandself.emailare tied to that specific userself.platformis accessed from the class-level but available to all instances
Imagine you’re building a REST API. This could map directly to your GET /user/{id} route.
Class methods get the class as the first argument, usually named cls. That means they can create or manipulate the class itself, not just instances.
- Alternate constructors like from a string, a dict, a config file etc.
- Factory patterns
- Managing class-level behavior or registration
Now let’s add one to our class.
class UserAccount:
platform = "MySaaSPlatform"
def __init__(self, username, email):
self.username = username
self.email = email
def get_profile(self):
return {
"username": self.username,
"email": self.email,
"platform": self.platform
}
@classmethod
def from_dict(cls, data):
return cls(data["username"], data["email"])And we can use it like below.
user_data = {"username": "bob", "email": "bob@example.com"}
user = UserAccount.from_dict(user_data)What’s happening here?
clshere refers toUserAccount, not an object.cls(...)creates a new instance. It’s useful if the class name ever changes or is inherited.
Let’s say your API receives JSON payloads. This is a great way to hydrate an object from them.
class AdminAccount(UserAccount):
def __init__(self, username, email, admin_level=1):
super().__init__(username, email)
self.admin_level = admin_leveladmin_data = {"username": "root", "email": "root@example.com"}
admin = AdminAccount.from_dict(admin_data)
print(type(admin))
# <class '__main__.AdminAccount'>Because we used cls, the factory works with subclasses too. That’s powerful.
Static methods don’t get self or cls. They’re just plain functions living inside a class.
- Utility functions that logically belong to the class
- When you want organization without inheritance or context
- Formatting, parsing, validating, etc.
Now let’s add this one too to out codebase
class UserAccount:
platform = "MySaaSPlatform"
def __init__(self, username, email):
self.username = username
self.email = email
def get_profile(self):
return {
"username": self.username,
"email": self.email,
"platform": self.platform
}
@classmethod
def from_dict(cls, data):
return cls(data["username"], data["email"])
@staticmethod
def is_valid_email(email):
return "@" in email and "." in emailAnd in practice we use it like below.
print(UserAccount.is_valid_email("test@company.com")) # TrueWhat’s happening?
- No access to
selforcls - Think of it as a “related tool” rather than behavior
In a real-life example, we can validate data before creating a user which is so useful:
data = {"username": "jane", "email": "janeexample.com"}
if UserAccount.is_valid_email(data["email"]):
user = UserAccount.from_dict(data)
else:
print("Invalid email format")Imagine you're building a user registration flow in your backend.
class UserAccount:
platform = "MySaaSPlatform"
def __init__(self, username, email):
self.username = username
self.email = email
def get_profile(self):
return {
"username": self.username,
"email": self.email,
"platform": self.platform
}
@classmethod
def from_request_payload(cls, payload: dict):
if not cls.is_valid_email(payload["email"]):
raise ValueError("Invalid email address.")
return cls(payload["username"], payload["email"])
@staticmethod
def is_valid_email(email):
return "@" in email and "." in emailYou can use this class in your creating new user flow like that:
def register_user(payload):
try:
user = UserAccount.from_request_payload(payload)
return user.get_profile()
except ValueError as e:
return {"error": str(e)}You’ve just:
- Validated email using a
staticmethod - Created a user with a
classmethod - Returned user info with an
instance method
Everything is where it belongs, and the code reads like a story.
| Mistake | Why It's a Problem |
|---|---|
Using @staticmethod when you actually need access to the class or instance |
Leads to rigid, unextendable code |
Using instance method (self) for something that doesn’t use instance state |
Misleads readers, makes testing harder |
Forgetting that classmethod respects inheritance |
Can lead to unexpected object types in factories |
| Type | First Arg | Accesses | Use Cases |
|---|---|---|---|
Instance (def) |
self |
instance + class attrs | Core behavior tied to object |
Class (@classmethod) |
cls |
class + static context | Alternative constructors, factories |
Static (@staticmethod) |
None | nothing automatically | Utilities, validators, formatters |
In modern Python applications, especially backend systems, the way you split logic into instance, class, and static methods can drastically affect readability, testability, and maintainability.
Think of it like this:
- Instance methods answer: “What can this user do?”
- Class methods answer: “How do I create or manage users?”
- Static methods answer: “How do I validate or process data related to users?”
Master this trio, and you'll find yourself writing cleaner, more scalable code with no more confusion, no more hacks.