Verification: —

Understanding `__slots__` with Metaclasses in Python

Exploring advanced behavior of `__slots__` via metaclasses, including memory implications and inheritance rules.

Contents

    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 doesn't show full impact
    # See: https://docs.python.org/3/reference/datamodel.html#__slots__
    # Also: https://docs.python.org/3/library/sys.html#sys.getsizeof

    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.

    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.

    Key Takeaways

    • Use __slots__ for memory-sensitive classes with many instances: Slotted objects save ~40–60 bytes per instance by eliminating __dict__. For classes with thousands of instances (ORM models, data records, game entities), the savings add up fast. For one-off objects, the optimization is negligible.
    • Metaclass auto-slots only sees class-level attributes: A metaclass’s __new__ receives attrs — a dict of class-level definitions. Instance attributes set in __init__ via self.x = 10 are invisible to the metaclass. Always declare slot-worthy attributes at class level if using metaclass automation.
    • Prefer explicit __slots__ over metaclass injection for most code: Manually writing __slots__ = ['x', 'y'] is simpler, more readable, and avoids metaclass complexity. Reserve metaclass slot patterns for framework code (ORMs, serializers, validators) where you control many class definitions.
    • Child classes must re-declare __slots__ for memory optimization: Subclasses don’t inherit __slots__ from parents. Each subclass needs its own __slots__ that includes parent slots. Omitting them silently re-adds __dict__, nullifying the memory savings.
    • sys.getsizeof() underreports __dict__ savings: Slotted objects appear only slightly smaller via getsizeof(), but the real win is avoiding the per-instance dict allocation. Use pympler.asizeof or tracemalloc for accurate cross-instance memory profiling.

    How to Apply This in Your Codebase

    1. Audit existing classes: Search for classes with many instances that don’t use __slots__ — rg -rn 'class \w+.*:' --include='*.py' | grep -v '__slots__'
    2. Add __slots__ to hot paths: Start with data classes, ORM models, and any class instantiated in loops
    3. Profile before and after: Use tracemalloc to measure actual memory reduction — don’t rely on sys.getsizeof() alone
    4. Test inheritance chains: Verify child classes re-declare __slots__ — missing declarations silently re-add __dict__
    5. When to use metaclass injection: Only for framework code where you control many class definitions and need consistent slot behavior