Understanding `__slots__` with Metaclasses in Python
Exploring advanced behavior of `__slots__` via metaclasses, including memory implications and inheritance rules.

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
- Class attributes and
__slots__: Only class-level attributes are checked by metaclasses - Inheritance behavior:
__slots__in parent classes do not automatically appear in__dict__in child classes - Attribute access timing:
__slots__restrictions apply at instance creation, not just during declaration
What I Got Wrong
- Initially, I assumed that any class with
xas an instance attribute would get__slots__from a metaclass. But metaclasses accessattrs, 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__receivesattrs— a dict of class-level definitions. Instance attributes set in__init__viaself.x = 10are 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 viagetsizeof(), but the real win is avoiding the per-instance dict allocation. Usepympler.asizeofortracemallocfor accurate cross-instance memory profiling.
How to Apply This in Your Codebase
- Audit existing classes: Search for classes with many instances that don’t use
__slots__—rg -rn 'class \w+.*:' --include='*.py' | grep -v '__slots__' - Add
__slots__to hot paths: Start with data classes, ORM models, and any class instantiated in loops - Profile before and after: Use
tracemallocto measure actual memory reduction — don’t rely onsys.getsizeof()alone - Test inheritance chains: Verify child classes re-declare
__slots__— missing declarations silently re-add__dict__ - When to use metaclass injection: Only for framework code where you control many class definitions and need consistent slot behavior