This in-depth guide demystifies the differences between instance, class, and static methods in Python. Through backend-focused examples, you'll learn when and why to use each method type from object behaviors to factory patterns and utility functions
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 = email
So 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.username
and self.email
are tied to that specific userself.platform
is accessed from the class-level but available to all instancesImagine 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.
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?
cls
here refers to UserAccount
, 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_level
admin_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.
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 email
And in practice we use it like below.
print(UserAccount.is_valid_email("test@company.com")) # True
What’s happening?
self
or cls
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 email
You 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:
staticmethod
classmethod
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:
Master this trio, and you'll find yourself writing cleaner, more scalable code with no more confusion, no more hacks.