Intermediate

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 Updated Suggest a fix

Quick answer

@classmethod receives the class (cls) and is used for alternate constructors; @staticmethod receives neither the object nor the class and is a helper that lives in the class; @property makes a method read like an attribute, so you can add validation without changing callers.

  • Instance methods get self, class methods get cls, static methods get neither.
  • A classmethod constructor respects subclasses: cls(...) builds the right type.
  • A property setter validates assignments such as obj.price = -1.
An alternate constructor, a helper, and a validated property
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    @classmethod
    def from_fahrenheit(cls, f):
        return cls(round((f - 32) * 5 / 9, 1))

    @staticmethod
    def is_freezing(celsius):
        return celsius <= 0

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("below absolute zero")
        self._celsius = value

t = Temperature.from_fahrenheit(212)
print(t.celsius, Temperature.is_freezing(t.celsius))

Output

100.0 False
// 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

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

Lesson 24 → Abstract Base Classes and Interfaces

Quick check

Which decorator suits a method that never uses self or cls?

HELP US IMPROVE

How was this lesson?

Your note is shared with the Chaduvuko team for review. Please don’t include personal or sensitive information.

Choose your feedback

Spotted a mistake or something unclear? Suggest a fix on GitHub or email us. Select the text first and it is quoted for you.

Share

Discussion

0

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

Continue with GitHub
Loading...