Not all methods belong to an instance. Some methods are logically associated with a class but operate on class-level data (@classmethod), and some are utility functions that happen to live in a class namespace but need no access to instance or class data (@staticmethod). Properties (@property) let you expose computed values as attributes rather than method calls, with optional setter logic for validation. These three decorators appear constantly in SQLAlchemy models (class-level query methods, column properties), Pydantic validators, and FastAPI dependency factories โ understanding them here pays off immediately when you start writing database models.
@classmethod โ Alternative Constructors
class User:
def __init__(self, name: str, email: str, role: str = "user"):
self.name = name
self.email = email.lower()
self.role = role
# @classmethod receives the class (cls) as first arg, not an instance
@classmethod
def from_dict(cls, data: dict) -> "User":
"""Create a User from a dictionary (e.g. from a JSON request body)."""
return cls(
name = data["name"],
email = data["email"],
role = data.get("role", "user"),
)
@classmethod
def from_google_oauth(cls, google_payload: dict) -> "User":
"""Create a User from a Google OAuth token payload."""
return cls(
name = google_payload["name"],
email = google_payload["email"],
role = "user", # OAuth users always start as 'user'
)
@classmethod
def create_admin(cls, name: str, email: str) -> "User":
return cls(name=name, email=email, role="admin")
# Usage โ multiple ways to create a User
user1 = User("Alice", "alice@example.com")
user2 = User.from_dict({"name": "Bob", "email": "bob@example.com"})
user3 = User.create_admin("Charlie", "charlie@example.com")
print(user3.role) # admin
@classmethod is conventionally named cls (for “class”) โ it receives the class itself, not an instance. This means classmethods work correctly with inheritance: if AdminUser inherits from User and calls AdminUser.from_dict(data), cls will be AdminUser, so the method returns an AdminUser instance, not a User. This is the key advantage of cls(...)) over hardcoding User(...) inside the classmethod.@classmethod for factory methods โ alternative ways to create an instance from different input formats. Common patterns: from_dict(), from_json()), from_csv_row(), from_env()). This keeps all creation logic inside the class and avoids scattered constructor calls with complex argument manipulation. FastAPI Pydantic models use a similar pattern with model_validate().@classmethod when the method needs access to instance data โ use a regular instance method. Do not use it when the method needs no class or instance data at all โ use @staticmethod instead. The rule: @classmethod needs the class (typically to create instances or access class-level config), @staticmethod is a plain utility function that just happens to live in the class namespace.@staticmethod โ Utility Functions in a Class
class User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
# @staticmethod โ no self or cls โ pure utility function
@staticmethod
def validate_email(email: str) -> bool:
"""Return True if email looks valid (basic check)."""
return "@" in email and "." in email.split("@")[-1]
@staticmethod
def hash_password(password: str) -> str:
import hashlib
return hashlib.sha256(password.encode()).hexdigest()
@staticmethod
def generate_username(name: str) -> str:
"""Generate a slug-style username from a full name."""
return name.lower().replace(" ", "_")
def set_password(self, password: str) -> None:
if len(password) < 8:
raise ValueError("Password must be at least 8 characters")
# Calls static method โ works from both instance and class
self._password_hash = User.hash_password(password)
# Call from the class โ no instance needed
print(User.validate_email("alice@example.com")) # True
print(User.validate_email("not-an-email")) # False
print(User.generate_username("Alice Smith")) # alice_smith
# Also callable from an instance (same result)
user = User("Alice", "alice@example.com")
print(user.validate_email("bob@example.com")) # True
@property โ Computed Attributes with Getter/Setter
class Post:
def __init__(self, title: str, body: str):
self._title = title # stored with _ prefix
self.body = body
self.published = False
# getter โ accessed as post.title (not post.title())
@property
def title(self) -> str:
return self._title
# setter โ called when post.title = "New Title"
@title.setter
def title(self, value: str) -> None:
if not value or len(value.strip()) < 3:
raise ValueError("Title must be at least 3 characters")
self._title = value.strip()
# Computed property โ no setter (read-only)
@property
def word_count(self) -> int:
return len(self.body.split())
@property
def slug(self) -> str:
return self._title.lower().replace(" ", "-")
@property
def is_ready_to_publish(self) -> bool:
return (
len(self._title) >= 3 and
self.word_count >= 50 and
not self.published
)
# Usage โ properties look like attributes (no parentheses)
post = Post("Hello World", "This is the body " * 10)
print(post.title) # Hello World
print(post.slug) # hello-world
print(post.word_count) # 50
print(post.is_ready_to_publish) # True (title ok, 50 words, not published)
post.title = " Updated Title " # triggers setter โ strips whitespace
print(post.title) # Updated Title
try:
post.title = "ab" # too short โ raises ValueError
except ValueError as e:
print(e) # Title must be at least 3 characters
Combining All Three in a Real Model
from datetime import datetime
class Post:
_all_posts: list = [] # class-level storage (in-memory for illustration)
def __init__(self, title: str, author: str):
self._title = title
self.author = author
self._tags = []
self.created = datetime.utcnow()
# โโ @property โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@property
def title(self) -> str:
return self._title
@title.setter
def title(self, v: str) -> None:
if not v.strip():
raise ValueError("Title cannot be empty")
self._title = v.strip()
@property
def tags(self) -> list:
return list(self._tags) # return copy โ prevents direct mutation
# โโ @classmethod โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@classmethod
def from_dict(cls, data: dict) -> "Post":
post = cls(data["title"], data["author"])
for tag in data.get("tags", []):
post._tags.append(tag.lower())
cls._all_posts.append(post)
return post
@classmethod
def get_all(cls) -> list:
return list(cls._all_posts)
# โโ @staticmethod โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@staticmethod
def validate_title(title: str) -> bool:
return 3 <= len(title.strip()) <= 200
Common Mistakes
Mistake 1 โ Using @property without the getter
โ Wrong โ trying to set a property with no setter defined:
@property
def title(self): return self._title
# No @title.setter defined
post.title = "New" # AttributeError: can't set attribute
โ Correct โ add the setter decorator:
@title.setter
def title(self, value: str): self._title = value.strip() # โ
Mistake 2 โ Calling a classmethod on an instance expecting instance data
โ Wrong โ classmethod cannot access instance attributes:
@classmethod
def describe(cls):
return self.name # NameError โ there is no 'self' in a classmethod!
โ Correct โ use a regular instance method for instance data:
def describe(self):
return self.name # โ regular instance method
Mistake 3 โ Property name conflicts with the backing attribute name
โ Wrong โ property and stored attribute use the same name, causing infinite recursion:
@property
def title(self): return self.title # RecursionError! Calls itself!
โ Correct โ back the property with a private attribute (underscore prefix):
@property
def title(self): return self._title # โ reads _title, not title
Quick Reference
| Decorator | First Param | Use For |
|---|---|---|
def method(self) |
self โ instance |
Instance behaviour, accessing instance data |
@classmethod |
cls โ class |
Alternative constructors, class-level operations |
@staticmethod |
none | Utility functions that belong in the class namespace |
@property |
self |
Computed read-only attributes, getter logic |
@name.setter |
self |
Validation and transformation on attribute assignment |