Skip to main content

Class methods, static methods & class attributes

Level 2: Level 2: Idiomsmedium12 minclassmethodstaticmethodclass-attributesclasses

Build alternative constructors with @classmethod, and know which state every instance shares.

Three kinds of method, three kinds of first argument

Every function in a class body is one of three things, and the decorator on it decides what Python passes as the first argument.

Written asFirst argumentReads or writes
def m(self)the instancethat one object's data
@classmethod def m(cls)the classdata shared by every instance
@staticmethod def m()nothingonly its own arguments
class User:
    role = "member"                 # class attribute: one copy, shared

    def __init__(self, name):
        self.name = name            # instance attribute: one per object

    def greet(self):                # instance method
        return f"{self.name} ({self.role})"

    @classmethod
    def from_csv(cls, line):        # alternative constructor
        return cls(line.split(",")[0])

    @staticmethod
    def is_valid(name):             # plain helper, namespaced on the class
        return len(name) > 0

print(User.from_csv("ada,42").greet())   # ada (member)
print(User.is_valid(""))                 # False

Why @classmethod is the alternative constructor

__init__ can only have one signature. Real data arrives in several shapes: a CSV line, a dict from JSON, a database row. A class method builds the object from each shape and hands the work to the single __init__.

Check yourself
from_csv is a @classmethod, but its body hard-codes the class name and returns User(line.split(',')[0]). class Admin(User) inherits it unchanged. What is type(Admin.from_csv('grace,7'))?

The first argument is cls, not the literal class name, and that difference matters. cls(...) builds whatever class the call was made on, so a subclass inherits the constructor for free:

class Admin(User):
    role = "admin"

print(type(Admin.from_csv("grace,7")))   # <class '__main__.Admin'>

Hard-coding User(...) inside from_csv would have returned a User even when called on Admin, silently discarding the subclass.

The class attribute that everyone shares

Check yourself
class Cart has items = [] written directly in the class body. You build a = Cart() and b = Cart(), then run a.items.append('apple'). What is b.items?

A class attribute lives on the class, so every instance sees the same object. Read that as a shared default and it is useful. Mutate it and every instance changes at once.

Names point at objects
Step 1 / 3
class Cart: items = []
a = Cart() b = Cart()
a.items.append("apple")
Names
  • Cart.itemsL1
Objects (heap)
  • L1list
    []

One list object, created once when the class body runs.

A mutable class attribute is shared state: appending through one instance is visible from every other.

The fix is to create the list per instance, inside __init__:

class Cart:
    def __init__(self):
        self.items = []     # a fresh list for every Cart
Check yourself
Same Cart, still with the shared class-level items = []. This time you write a.items = ['x'] instead of appending. What is b.items now?

Pitfalls

  • Mutable class attributes. items = [] in the class body makes one list for the whole program. Use it for genuine constants (role = "member", MAX_RETRIES = 3) and build mutable state in __init__.
  • Assignment does not mutate. a.items = ["x"] creates a new instance attribute that shadows the class one, so b.items is unaffected. Only mutation (append, += on a list) leaks across instances, which is why the bug is so easy to miss.
  • Hard-coding the class inside a classmethod. Return cls(...), never User(...), or subclasses get the wrong type back.
  • Reaching for @staticmethod too often. If it touches neither self nor cls, ask whether it wants to be a module-level function. Keep it a static method only when the class is the natural place a reader would look for it.

Interview nuance: "why is cls better than the class name" tests whether you understand that Python resolves attributes at call time through the instance's own class. cls is polymorphic; the literal name is not. The same reasoning explains why super().__init__() beats ParentClass.__init__(self): both keep the inheritance chain intact instead of pinning one link in it.

Worked example (Python)
class Cart:
    items = []          # shared by every instance

a = Cart()
b = Cart()
a.items.append("apple")
print(b.items)          # ['apple'] - one list, not two

Apply

Your turn

The task this lesson builds to.

Write a @classmethod named from_csv that builds a User from a "name,age" string and returns the user's name.

run("ada,36") should return "ada". Build the object with cls, not User.

3 hints and 4 automated checks are waiting in the workspace.

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Fix Cart so each cart has its own item list, then return the number of items in the second cart.

run("apple") should return 0: adding to the first cart must not touch the second.

3 hints and 3 automated checks are waiting in the workspace.