Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
Intermediate+150 XP

Class Methods, Static Methods and Properties

@classmethod, @staticmethod, and @property — what each is actually for, with real examples of when to reach for each.

35 min August 2026
// Part 01 — Three Kinds of Method, One Class

Instance Methods vs Class Methods vs Static Methods

Every method you have written so far has been an instance method — it takes self as its first parameter and operates on one specific object. Python offers two other kinds of method, each with a different relationship to the class, and each declared with a decorator.

All three side by side
class Pizza:
    def __init__(self, toppings):
        self.toppings = toppings

    def describe(self):                     # instance method — needs a specific pizza
        return f"Pizza with {', '.join(self.toppings)}"

    @classmethod
    def margherita(cls):                    # class method — receives the CLASS, not an instance
        return cls(["tomato", "mozzarella", "basil"])

    @staticmethod
    def is_valid_topping(topping):          # static method — receives NEITHER
        return topping.lower() not in {"pineapple"}

p = Pizza.margherita()          # an alternate way to construct a Pizza, no instance needed yet
print(p.describe())             # "Pizza with tomato, mozzarella, basil"
print(Pizza.is_valid_topping("pineapple"))   # False

The distinction is entirely about what the method automatically receives as its first argument: an instance method receives the specific object (self); a class method receives the class itself (cls) — useful when the logic needs to know about the class but not about any particular instance; a static method receives neither — it is really just a regular function that happens to live inside the class's namespace for organisational purposes.

// Part 02 — @classmethod as Alternate Constructors

The Single Most Common Real Use of @classmethod

Python only allows one __init__ per class, but real-world objects often need to be built from several different kinds of input — a raw dict from an API, a CSV row, a set of sensible defaults. @classmethod alternate constructors are the standard way to offer several named ways to build an object, all funnelling into the same __init__.

Alternate constructors — a genuinely common real pattern
class User:
    def __init__(self, username, email, is_admin=False):
        self.username = username
        self.email = email
        self.is_admin = is_admin

    @classmethod
    def from_api_response(cls, data):
        return cls(
            username=data["user_name"],
            email=data["contact_email"],
            is_admin=data.get("role") == "admin",
        )

    @classmethod
    def guest(cls):
        return cls(username="guest", email="")

api_data = {"user_name": "asha", "contact_email": "asha@example.com", "role": "admin"}
u1 = User.from_api_response(api_data)
u2 = User.guest()

The reason this is a classmethod and not just a plain standalone function: cls refers to whichever class it was actually called on, so if AdminUser subclasses User, calling AdminUser.from_api_response(data) correctly builds an AdminUser instance, not a plain User — a standalone function hardcoding User(...) could never do that.

// Part 03 — @staticmethod as Namespacing

When You Just Want a Function to Live Near Its Class

A static method is, functionally, just a plain function — it does not receive self or cls, and it cannot access or modify instance or class state directly. Its only real purpose is organisation: grouping a piece of logic that is conceptually related to the class, even though it does not need any of the class's data.

A validator that doesn't need any instance or class data
class Pizza:
    @staticmethod
    def is_valid_topping(topping):
        banned = {"pineapple", "anchovy"}
        return topping.lower() not in banned

# Callable from the class, without needing to build a Pizza first:
print(Pizza.is_valid_topping("mushroom"))   # True

# Also callable from an instance — works, but doesn't use the instance at all
p = Pizza()
print(p.is_valid_topping("pineapple"))      # False
🎯 Pro Tip
A useful gut check: if a method never touches self or cls, it should almost always be a @staticmethod — writing it as a regular instance method that simply never uses self is a common, low-severity code smell that reviewers frequently flag, since it misleadingly implies the method depends on instance data when it does not.
// Part 04 — @property

Computed Attributes That Look Like Plain Data

@property lets a method be accessed with plain attribute syntax — no parentheses — which is useful for exposing a computed value that reads naturally as data, while still running real code underneath.

Without @property — a getter method, called like a method
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def get_area(self):
        return self.width * self.height

r = Rectangle(4, 5)
print(r.get_area())     # 20 — has to be called as a method
With @property — reads like a plain attribute
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    @property
    def area(self):
        return self.width * self.height

r = Rectangle(4, 5)
print(r.area)            # 20 — no parentheses! Reads exactly like a data attribute
r.area = 30               # AttributeError — this property has no setter yet (see below)

Setters and deleters — controlling assignment, not just reads

Adding a setter with validation
class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self.height = height

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError("width must be positive")
        self._width = value

r = Rectangle(4, 5)
r.width = 10        # runs the setter — validated
r.width = -3        # ValueError: width must be positive

This is exactly why @property matters beyond convenience: it lets a class start as plain public attributes, and later add validation or computed logic without breaking any code that already does rectangle.width = 10 — the calling code's syntax never has to change, only the class's internals do. This is a genuinely important design property (sometimes called "uniform access") that a language without properties, forcing every external caller to use get_width()/set_width() from day one, does not offer.

// Part 05 — Real World
💼 What This Looks Like at Work

Refactoring a Public API Without Breaking Callers, at an Austin SaaS Company

Scenario — SaaS company, Austin · Backwards-compatible internal refactor

A Subscription class originally stores monthly_price as a plain public attribute, used directly across dozens of call sites throughout the codebase. A new business rule arrives: the price must never be set below the plan's configured minimum, and every assignment needs to log a price-change event for billing audit purposes.

Before — a plain attribute, used everywhere as sub.monthly_price = 29.99
class Subscription:
    def __init__(self, plan_minimum, monthly_price):
        self.plan_minimum = plan_minimum
        self.monthly_price = monthly_price
After — a property, with validation and logging, same external syntax
class Subscription:
    def __init__(self, plan_minimum, monthly_price):
        self.plan_minimum = plan_minimum
        self._monthly_price = monthly_price

    @property
    def monthly_price(self):
        return self._monthly_price

    @monthly_price.setter
    def monthly_price(self, value):
        if value < self.plan_minimum:
            raise ValueError(f"Price cannot go below the plan minimum of ${self.plan_minimum}")
        log_price_change(self._monthly_price, value)
        self._monthly_price = value

Why this mattered

Every one of the dozens of existing call sites reading or writing sub.monthly_price = ... continued working with zero changes — the property is indistinguishable from a plain attribute to calling code. Had the class instead switched to get_monthly_price()/set_monthly_price() methods (the pattern from languages without properties), every single call site across the codebase would have needed updating in the same PR — a far larger, riskier change for what is fundamentally an internal implementation detail.

// Part 06 — Misconceptions

Four Misconceptions About Class/Static Methods and Properties

✕ ""@staticmethod and @classmethod are basically interchangeable""
A classmethod receives the class itself (cls) as its first argument and can construct/reference the class dynamically (important for subclassing); a staticmethod receives neither and cannot reference the class at all without hardcoding its name. Reach for classmethod for alternate constructors, staticmethod for a genuinely standalone helper.
✕ ""You should add @property to every attribute from the start, just in case""
Most attributes should simply be plain public attributes — that is the idiomatic Python default. Reach for @property specifically when you need validation, a computed value, or logging on read/write, not preemptively for attributes that are genuinely just data.
✕ ""A static method inside a class can still access instance attributes if it needs to""
It genuinely cannot — it has no access to self or cls at all. If a method needs any instance or class data, it must be an instance method or classmethod respectively, not a staticmethod.
✕ ""@classmethod is mainly useful for factory/alternate constructors, nothing else""
That is its most common use, but classmethods are also used for methods that need to operate on class-level state shared across all instances (like a running count of created instances), and in some inheritance-aware patterns where a method must know which subclass it was actually called on.
// Part 07 — Interview Prep

5 Interview Questions — With Complete Answers

What is the difference between @classmethod and @staticmethod?
A classmethod automatically receives the class itself as its first argument (conventionally named cls), letting it construct instances of the class or reference class-level state — and importantly, cls refers to whichever class it was actually called on, correctly supporting subclasses. A staticmethod receives neither self nor cls; it is effectively a plain function namespaced under the class purely for organisation.
Give a real, common use case for a @classmethod.
Alternate constructors — a class often needs to be built from several different kinds of input (a raw API dict, a CSV row, sensible defaults) while __init__ can only have one signature. A classmethod like from_api_response(cls, data) builds and returns an instance via cls(...), giving several named ways to construct the same class.
What does @property actually change about how a method is called?
It lets a method be accessed using plain attribute syntax (no parentheses) — obj.area instead of obj.area(). This allows a class to expose validated or computed values that read exactly like data attributes to calling code.
Why would you convert a plain public attribute into a @property later, instead of just leaving it public?
It lets you add validation, computed logic, or side effects (like logging) on read/write WITHOUT changing the external syntax anyone else's code uses to access it — existing call sites doing obj.attr or obj.attr = value keep working unchanged, unlike switching to get_/set_ methods, which would require updating every call site.
What happens if you assign to a @property that only has a getter defined, no setter?
Python raises an AttributeError, because a property without an explicit @x.setter is effectively read-only from outside the class — attempting obj.prop = value has no defined behaviour to run.
// Common Mistakes

Method Decorator Mistakes Beginners Make Constantly

Forgetting self on a regular instance method
def describe(): rather than def describe(self): raises a TypeError the moment it is called on an instance, since Python always passes the instance as the first argument automatically for instance methods.
Using @staticmethod when @classmethod was actually needed
A staticmethod-based "alternate constructor" that hardcodes the class name (return Pizza(...)) breaks for any subclass — calling SpecialPizza.margherita() would still return a plain Pizza, not a SpecialPizza. Use @classmethod with cls(...) whenever subclassing should be respected.
Naming the backing attribute the same as the property
def width(self): return self.width inside a @property causes infinite recursion (RecursionError) — the getter calls itself. The backing attribute must have a different name, conventionally with a leading underscore (self._width).
Adding a @property setter that does validation but forgetting the getter still needs to exist
A @x.setter decorator requires a property named x to already be defined via @property above it — defining only the setter without the getter raises a NameError, since there is no property named x yet to attach the setter to.
// Error Library

Errors You Will Hit With Method Decorators — And Exactly Why

TypeError: describe() takes 0 positional arguments but 1 was given
Cause: An instance method was defined without self as its first parameter, but Python still automatically passes the instance when it is called normally.
Fix: Add self as the first parameter, or add @staticmethod above it if it genuinely should not receive the instance.
AttributeError: can't set attribute 'area'
Cause: Attempting to assign to a @property that has no @x.setter defined — it is effectively read-only.
Fix: Add a setter with @propertyname.setter if assignment should be allowed, or stop trying to assign to a value that is meant to be purely computed.
RecursionError: maximum recursion depth exceeded
Cause: A @property getter (or setter) refers to self.<same_name> internally, calling itself infinitely instead of the intended backing attribute.
Fix: Store the real value under a differently-named backing attribute, typically with a leading underscore (self._width), and have the property read/write that instead of its own name.
NameError: name 'width' is not defined
Cause: @width.setter was used without a preceding @property-decorated method named width already defined in the class.
Fix: Define the getter first with @property before adding a matching @width.setter beneath it.

🎯 Key Takeaways

  • Instance methods receive self (the specific object); classmethods receive cls (the class itself); staticmethods receive neither.
  • @classmethod is most commonly used for alternate constructors (User.from_api_response(data)) — and correctly respects subclasses, unlike a hardcoded standalone function.
  • @staticmethod is for a helper that is conceptually related to the class but never touches instance or class state — if a method never uses self or cls, it is a strong candidate for @staticmethod.
  • @property lets a method be accessed with plain attribute syntax, enabling validation or computed values without breaking existing calling code that reads/writes it like a normal attribute.
  • The backing attribute behind a property must have a different name than the property itself (typically a leading underscore) to avoid infinite recursion.
  • A property with no @x.setter is effectively read-only from outside the class, raising AttributeError on assignment.

What comes next

Module 24 closes out the Object-Oriented Python phase with abstract base classes — enforcing a contract across subclasses with Python's abc module.

Module 24 → Abstract Base Classes and Interfaces
Share

Discussion

0

Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.

Continue with GitHub
Loading...