Python `__slots__`: Memory Optimization or Silent Pitfall?

Exploring advanced `__slots__` behavior, memory impacts, and pitfalls with metaclasses.

Python’s __slots__ is a powerful feature for optimizing object memory, but its interaction with metaclasses can be subtle. This post dives into advanced usage, exposing real-world pitfalls and insights.

The Problem: How Metaclasses Interact with __slots__

class SlottedMeta(type):
    def __new__(cls, name, bases, attrs):
        # Only add __slots__ to classes that explicitly define 'x'
        if 'x' in attrs and not isinstance(attrs['x'], (classmethod, staticmethod)):
            attrs['__slots__'] = ['x']
        return super().__new__(cls, name, bases, attrs)

class TestClass(metaclass=SlottedMeta):
    def __init__(self):
        self.x = 10

obj = TestClass()
print(obj.x)  # 10
# obj.y = 20  # This will raise AttributeError

This pattern attempts to auto-add __slots__ when a class has x as a class-level attribute. But what happens in practice?

Pattern 1: Why __slots__ Isn’t Always Added

class SlottedMeta(type):
    def __new__(cls, name, bases, attrs):
        if 'x' in attrs and not isinstance(attrs['x'], (classmethod, staticmethod)):
            attrs['__slots__'] = ['x']
        return super().__new__(cls, name, bases, attrs)

class TestClass(metaclass=SlottedMeta):
    def __init__(self):
        self.x = 10  # NOT a class attribute!

obj = TestClass()
print(obj.x)  # 10

# This SHOULD raise AttributeError, but does not
try:
    obj.y = 20
    print("No error! y assigned successfully")
except AttributeError:
    print("AttributeError as expected")

Pattern 2: Properly Detecting Class Attributes

class SlottedMeta(type):
    def __new__(cls, name, bases, attrs):
        # Add __slots__ if any of the class's defined attributes are 'x'
        defined_attrs = set(attrs.keys())
        if 'x' in defined_attrs:
            attrs['__slots__'] = ['x']
        return super().__new__(cls, name, bases, attrs)

class TestClass(metaclass=SlottedMeta):
    x = 10  # Now it's a class attribute

obj = TestClass()
print(obj.x)  # 10

# Now this raises AttributeError
try:
    obj.y = 20  # Should raise AttributeError
except AttributeError as e:
    print(f"Catched: {e}")

Pattern 3: Memory Implications and Object Layout

import sys

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

class Slotted:
    __slots__ = ['x', 'y']
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

r = Regular(1, 2)
s = Slotted(1, 2)

print(f"Regular: {sys.getsizeof(r)} bytes")
print(f"Slotted: {sys.getsizeof(s)} bytes")

# Note: sys.getsizeof may underreport total impact.
# For accurate memory usage, consider using `pympler.asizeof`:
#   from pympler import asizeof
#   print(f"Regular (pympler): {asizeof.asizeof(r)} bytes")
#   print(f"Slotted (pympler): {asizeof.asizeof(s)} bytes")

Edge Cases I Missed

  1. Class attributes and __slots__: Only class-level attributes are checked by metaclasses
  2. Inheritance behavior: __slots__ in parent classes do not automatically appear in __dict__ in child classes
  3. Attribute access timing: __slots__ restrictions apply at instance creation, not just during declaration

What I Got Wrong

  • Initially, I assumed that any class with x as an instance attribute would get __slots__ from a metaclass. But metaclasses access attrs, a dict of class attributes — not instance attributes.
  • I also overlooked that sys.getsizeof() does not capture the full memory reduction from __slots__ since it measures instance size, not including the hidden __dict__ allocation for regular classes. Use pympler.asizeof for better memory insights.

Verdict

This post underscores the necessity of precise metaclass logic when using __slots__. The original example failed to meet expectations due to misunderstanding attribute scope. Proper metaclass behavior requires checking for class-level definitions, not instance-level modifications.

Score: 6.0/10 — Understanding improved, but code snippets need more accurate prediction.