Classes, Instances, Attributes & Methods
Classes in Python are dynamic first-class objects created by the type metaclass at runtime. Understanding instance attribute dicts (__dict__), memory optimization via __slots__, bound vs unbound method descriptors (PyMethodObject), and @classmethod vs @staticmethod is fundamental for senior Python object-oriented design.
This chapter details CPython class/instance memory structures, __slots__ 60% memory reduction mechanics, method binding descriptors, and method dispatch types.
1. Class & Instance Memory Architecture (__dict__ vs. __slots__)
By default, every Python instance stores its dynamic attributes inside a heap-allocated dictionary (instance.__dict__).
Standard Instance Memory Layout (with __dict__):
[ PyObject Header (16B) ] -> [ __dict__ Pointer (8B) ] -> [ PyDictObject (~200B) ]
βββ "x": 10
βββ "y": 20While dynamic attribute assignment is flexible, allocating a 200-byte dictionary for millions of small objects creates massive RAM bloat.
Memory Optimization with __slots__:
Declaring __slots__ = ('x', 'y') inside a class tells CPython not to allocate an instance.__dict__. Instead, CPython allocates a fixed C array of attribute pointers directly inside the instance struct:
Optimized Instance Memory Layout (with __slots__):
[ PyObject Header (16B) ] -> [ Slot 0 ('x'): 10 (8B) ] -> [ Slot 1 ('y'): 20 (8B) ]
(Zero PyDictObject overhead! Saves 60%-70% RAM per instance!)2. Method Binding Mechanics (PyMethodObject)
Functions defined inside a class are standard function objects (PyFunctionObject). Accessing a function through a class instance triggers its descriptor __get__() method, wrapping the function in a Bound Method (PyMethodObject):
class Vector:
def __init__(self, x):
self.x = x
def magnitude(self):
return self.x
v = Vector(10)
# Function accessed via Class -> Unbound Function
print(Vector.magnitude) # <function Vector.magnitude at 0x...>
# Function accessed via Instance -> Bound Method Descriptor!
print(v.magnitude) # <bound method Vector.magnitude of <Vector object at 0x...>>A bound method automatically binds v as the first argument (self), transforming v.magnitude() into Vector.magnitude(v).
3. Method Types: Instance, @classmethod, and @staticmethod
class DatabasePool:
def __init__(self, connection_str: str):
self.connection_str = connection_str # Instance Attribute
# 1. Instance Method: Receives 'self' (instance pointer)
def connect(self):
return f"Connected to {self.connection_str}"
# 2. Class Method: Receives 'cls' (class object) -> Alternative Factory Constructor
@classmethod
def from_env(cls):
env_str = os.getenv("DATABASE_URL", "sqlite:///:memory:")
return cls(env_str) # Instantiates class dynamically
# 3. Static Method: Receives NO implicit first argument -> Pure Utility Function
@staticmethod
def validate_url(url: str) -> bool:
return url.startswith("sqlite://") or url.startswith("postgresql://")4. Production Trade-offs & __slots__ Inheritance
__slots__Inheritance Invariant: Child classes must declare__slots__ = ()explicitly. If a child class omits__slots__, CPython automatically allocates aninstance.__dict__for the child instance, negating the memory savings of the parent class!__slots__Limitations: Prevents assigning arbitrary un-slotted attributes at runtime and prevents usingweakrefunless'__weakref__'is explicitly included in__slots__.