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.
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 forBankAccount.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
| Feature | Instance Method | Class Method (@classmethod) | Static Method (@staticmethod) |
|---|---|---|---|
| Decorator | None (Default) | @classmethod | @staticmethod |
| First Argument | self (Instance) | cls (Class / Type) | None (Plain arguments only) |
| Implicit Binding | Bound to instance | Bound to class | Unbound (Plain function) |
Can Access Instance State (self)? | Yes | No | No |
Can Access Class State (cls)? | Yes (via self.__class__) | Yes (via cls) | No (unless hardcoded) |
| Primary Use Case | Modifying object state & behavior | Factory constructors, class-level config | Utility 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__
- Memory Efficiency: Reduces per-instance memory consumption by 40% to 60% by eliminating
__dict__and__weakref__. - Faster Access: Attribute lookups on slotted classes are measurably faster than dictionary lookups.
- 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' - Inheritance Caveat: Subclasses do not inherit the slotted behavior automatically. If a subclass of
SlottedPointdoes not define its own__slots__, Python will automatically allocate a__dict__for subclass instances.
A developer defines the following class hierarchy:
What is printed when this script runs?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__)
Consider the following class with a managed property:
What is the result of attempting to execute class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
c = Circle(5)
c.radius = 10
c.radius = 10?
Which of the following method declarations correctly defines a utility function that validates data without accessing or modifying instance or class state?
Given the following class definition using __slots__:
What occurs when the line class Coordinate:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
pt = Coordinate(1, 2)
pt.z = 3
pt.z = 3 is executed?