Class Methods, Static Methods and Properties

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
Note: The first parameter of a @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.
Tip: Use @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().
Warning: Do not use @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

🧠 Test Yourself

You want to add a method to your Post class that checks whether a given string is a valid slug format (lowercase, hyphens only). It needs no access to any post data. Which decorator should you use?