6.3 Class Methods, Static Methods, and Properties

Key Takeaways

  • Instance methods receive `self` as their implicit first parameter and operate directly on instance state and class attributes.
  • Class methods are decorated with `@classmethod`, receive the class object `cls` as their implicit first parameter, and serve as alternative constructors / factory methods that support inheritance.
  • Static methods are decorated with `@staticmethod`, receive no implicit first argument (`self` or `cls`), and function as plain utility functions namespaced within a class.
  • The `@property` decorator transforms methods into managed attributes, supporting getter, setter (`@name.setter`), and deleter (`@name.deleter`) logic while retaining standard dot notation syntax.
  • The `__slots__` class variable restricts attribute creation, eliminates the per-instance `__dict__` dictionary, and drastically reduces memory consumption.
Last updated: August 2026

Class Methods, Static Methods, and Properties

In standard Python class definitions, methods are bound to specific object instances. However, real-world object-oriented architecture requires a diverse toolkit of method types and encapsulation mechanisms to manage class-level data, implement factory constructors, provide namespaced utilities, and enforce strict attribute validation.

Python provides three primary built-in method decorators (@classmethod, @staticmethod, and @property) along with the memory optimization mechanism __slots__ to fine-tune how classes and instances interact with data.


1. Instance Methods vs. Class Methods vs. Static Methods

Every method declared inside a class body is classified based on how the Python descriptor protocol binds its arguments when invoked.

A. Instance Methods (self)

Instance methods are the default method type in Python. When invoked on an instance, Python automatically passes the instance itself as the implicit first argument (self):

class BankAccount:
    def __init__(self, account_holder, balance=0.0):
        self.account_holder = account_holder
        self.balance = balance

    # Instance Method: Bound to the instance (self)
    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit amount must be positive.")
        self.balance += amount
        return self.balance
  • Binding: Bound to the specific instance (acc.deposit(100) is syntactic sugar for BankAccount.deposit(acc, 100)).
  • Access: Can access and modify both instance state (self.balance) and class state (self.__class__.interest_rate).

B. Class Methods (@classmethod and cls)

A class method is decorated with @classmethod. When invoked, Python automatically passes the class object itself (the type) as the implicit first argument, conventionally named cls:

class BankAccount:
    interest_rate = 0.035  # Class attribute (3.5% APY)

    def __init__(self, account_holder, balance=0.0):
        self.account_holder = account_holder
        self.balance = balance

    # Class Method: Bound to the class (cls)
    @classmethod
    def set_interest_rate(cls, new_rate):
        if new_rate < 0:
            raise ValueError("Rate cannot be negative.")
        cls.interest_rate = new_rate

    # Alternative Constructor / Factory Method
    @classmethod
    def from_csv_string(cls, data_row):
        """Create a BankAccount instance from a 'Name,Balance' string."""
        name, balance_str = data_row.split(",")
        # Uses cls(...) so subclasses instantiate the correct derived type
        return cls(name.strip(), float(balance_str.strip()))

Calling Semantics

Class methods can be called directly on the class (BankAccount.set_interest_rate(0.04)) or on an instance (acc.set_interest_rate(0.04)). In both cases, cls receives the class object (BankAccount), never the instance.

Why Factory Methods Use cls(...) Instead of ClassName(...)

When an alternative constructor uses cls(...), it supports subclass polymorphism. If a subclass SavingsAccount inherits from_csv_string(), calling SavingsAccount.from_csv_string("Bob,500") automatically constructs an instance of SavingsAccount, rather than hardcoding BankAccount.


C. Static Methods (@staticmethod)

A static method is decorated with @staticmethod. Static methods receive no implicit first argument—neither self nor cls is passed:

class BankAccount:
    # Static Method: Plain function logically namespaced within the class
    @staticmethod
    def validate_routing_number(routing_number):
        """Utility function checking if routing number is a 9-digit string."""
        return isinstance(routing_number, str) and len(routing_number) == 9 and routing_number.isdigit()

# Invocation on Class or Instance
print(BankAccount.validate_routing_number("123456789"))  # Output: True
print(BankAccount.validate_routing_number("invalid"))    # Output: False

Static methods are used when a utility function is conceptually related to the class domain, but does not need to read or modify instance attributes or class attributes.


Comparison Table: Python Method Types

FeatureInstance MethodClass Method (@classmethod)Static Method (@staticmethod)
DecoratorNone (Default)@classmethod@staticmethod
First Argumentself (Instance)cls (Class / Type)None (Plain arguments only)
Implicit BindingBound to instanceBound to classUnbound (Plain function)
Can Access Instance State (self)?YesNoNo
Can Access Class State (cls)?Yes (via self.__class__)Yes (via cls)No (unless hardcoded)
Primary Use CaseModifying object state & behaviorFactory constructors, class-level configUtility functions & validators

2. Managed Attributes with @property

In many object-oriented languages (like Java or C++), developers write explicit get_x() and set_x() methods to protect private variables. In Python, the idiomatic convention is to expose public attributes directly (obj.x), and convert them to managed properties using @property if validation or computed behavior is required later—without breaking the external public API.

Property Getters, Setters, and Deleters

The @property decorator turns a method into a read-only getter. Additional decorators (@name.setter and @name.deleter) define mutation and deletion logic:

class Temperature:
    def __init__(self, celsius=0.0):
        # Calls the property setter below to enforce validation
        self.celsius = celsius

    # 1. GETTER: Accessed like an attribute (temp.celsius)
    @property
    def celsius(self):
        return self._celsius

    # 2. SETTER: Triggered on assignment (temp.celsius = 25)
    @celsius.setter
    def celsius(self, value):
        if not isinstance(value, (int, float)):
            raise TypeError("Temperature must be numeric.")
        if value < -273.15:
            raise ValueError("Temperature cannot be below absolute zero (-273.15°C)!")
        self._celsius = float(value)

    # 3. DELETER: Triggered on del (del temp.celsius)
    @celsius.deleter
    def celsius(self):
        print("Resetting temperature to 0.0°C...")
        self._celsius = 0.0

    # Computed Read-Only Property (No setter defined!)
    @property
    def fahrenheit(self):
        return (self._celsius * 9 / 5) + 32

Operational Behavior

t = Temperature(25)
print(t.celsius)     # Output: 25.0 (Invokes getter)
print(t.fahrenheit)  # Output: 77.0 (Invokes computed getter)

t.celsius = 100      # Invokes setter with validation
print(t.fahrenheit)  # Output: 212.0

# Read-Only Property Enforcement:
try:
    t.fahrenheit = 500  # No @fahrenheit.setter exists!
except AttributeError as err:
    print("AttributeError:", err)  # AttributeError: property 'fahrenheit' of 'Temperature' object has no setter

# Validation Enforcement:
try:
    t.celsius = -300
except ValueError as err:
    print("Validation error:", err)  # Validation error: Temperature cannot be below absolute zero...

3. Memory Optimization with __slots__

By default, every Python instance stores its instance attributes inside a private dictionary accessible via instance.__dict__. While this enables dynamic attribute creation at runtime (obj.new_attr = 123), dictionaries incur noticeable memory overhead due to hash table allocation.

When an application creates millions of small objects (such as points in 3D geometry or financial market ticks), dictionary overhead can exhaust system memory.

The __slots__ Declaration

Defining __slots__ at class scope instructs Python to allocate a fixed-size array of descriptor references instead of creating a __dict__ for each instance:

class StandardPoint:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class SlottedPoint:
    __slots__ = ('x', 'y')  # Sequence of allowable attribute strings

    def __init__(self, x, y):
        self.x = x
        self.y = y

p1 = StandardPoint(10, 20)
p2 = SlottedPoint(10, 20)

print(hasattr(p1, '__dict__'))  # Output: True (Has instance dictionary)
print(hasattr(p2, '__dict__'))  # Output: False (No __dict__ allocated!)

Key Mechanics and Restrictions of __slots__

  1. Memory Efficiency: Reduces per-instance memory consumption by 40% to 60% by eliminating __dict__ and __weakref__.
  2. Faster Access: Attribute lookups on slotted classes are measurably faster than dictionary lookups.
  3. Attribute Restriction: Instances are strictly prohibited from creating attributes not explicitly declared in __slots__:
    p2.z = 30  # Raises AttributeError: 'SlottedPoint' object has no attribute 'z'
    
  4. Inheritance Caveat: Subclasses do not inherit the slotted behavior automatically. If a subclass of SlottedPoint does not define its own __slots__, Python will automatically allocate a __dict__ for subclass instances.
Loading diagram...
Method Dispatch and Parameter Binding Architecture
Test Your Knowledge

A developer defines the following class hierarchy:

class Document:
    @classmethod
    def create(cls, title):
        return cls(title)

    def __init__(self, title):
        self.title = title

class Spreadsheet(Document):
    pass

sheet = Spreadsheet.create("Q3_Budget")
print(type(sheet).__name__)
What is printed when this script runs?

A
B
C
D
Test Your Knowledge

Consider the following class with a managed property:

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

c = Circle(5)
c.radius = 10
What is the result of attempting to execute c.radius = 10?

A
B
C
D
Test Your Knowledge

Which of the following method declarations correctly defines a utility function that validates data without accessing or modifying instance or class state?

A
B
C
D
Test Your Knowledge

Given the following class definition using __slots__:

class Coordinate:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

pt = Coordinate(1, 2)
pt.z = 3
What occurs when the line pt.z = 3 is executed?

A
B
C
D