Alright, let’s talk about one of Python’s open secrets for squeezing memory efficiency out of your objects: __slots__. If you’re creating millions of instances of a class (and you’ll know you are because your program will have started to sound like a hairdryer), the default way Python handles instance attributes becomes a real problem.

By default, Python uses a dictionary (__dict__) to store an object’s attributes. This is fantastically flexible—you can add, remove, and modify attributes on the fly. It’s the reason we can do crazy things like obj.new_attr_i_just_made_up = 42 at runtime. But that flexibility comes at a cost. A dict has its own overhead: it’s a hash table that needs to pre-allocate memory to be efficient. For a single object, it’s negligible. For a million objects, that overhead adds up to a staggering amount of wasted RAM.

__slots__ is our way of telling the Python interpreter, “Hey, cut it out. This class is a serious, formal affair. My instances will only ever have these specific attributes, and I won’t be doing any of that monkey-patching nonsense.” You define __slots__ as a class variable, and it’s a simple tuple of the attribute names you’ll allow.

class RegularUser:
    def __init__(self, name, user_id):
        self.name = name
        self.user_id = user_id

class SlotsUser:
    __slots__ = ('name', 'user_id')
    def __init__(self, name, user_id):
        self.name = name
        self.user_id = user_id

# Let's see the memory difference
import sys
regular_instance = RegularUser('Guido', 1)
slots_instance = SlotsUser('Guido', 1)

print(f"Regular dict size: {sys.getsizeof(regular_instance.__dict__)} bytes")
print(f"Slots instance size: {sys.getsizeof(slots_instance)} bytes")

On a standard 64-bit Python build, you might see something like Regular dict size: 104 bytes for the __dict__ alone, plus the base object size. The SlotsUser instance might be around 48 bytes total. The SlotsUser instance has no __dict__. The interpreter pre-allocates a fixed, tiny amount of memory for the precisely defined attributes right inside the instance itself. This is the core of the savings.

How It Actually Works Under the Hood

When you define __slots__, the Python interpreter doesn’t create a __dict__ for each instance. Instead, it creates fixed, C-style array-like descriptors for the attributes you listed. When you access obj.name, the interpreter knows exactly where to find that value in the object’s memory layout without going through a dictionary lookup. This isn’t just about memory; it can also give you a slight attribute access speed boost, though that’s usually a secondary benefit.

The Gotchas (And Oh, There Are Gotchas)

This power comes with significant trade-offs. The designers made choices here, and some of them are…questionable.

  1. No Arbitrary Attributes: This is the big one. You can’t add new attributes to a __slots__ instance. The __dict__ is gone. If you try slots_instance.email = 'guido@python.org', you’ll get an AttributeError. You traded flexibility for efficiency. Plan accordingly.

  2. Inheritance is a Minefield: The rules around __slots__ and inheritance are, frankly, a bit weird. If a child class doesn’t define its own __slots__, it will inexplicably get a __dict__ anyway, nullifying the memory benefit. If it does define its own __slots__, it only contains the attributes it adds. The parent’s slots are already handled.

class Parent:
    __slots__ = ('a',)

class ChildWithDict(Parent):
    pass  # This child has a __dict__ and __weakref__! Memory wasted.

class ChildWithSlots(Parent):
    __slots__ = ('b',)  # This child only has slots for 'b'. The slot for 'a' is from Parent.

child_dict = ChildWithDict()
child_dict.a = 1
child_dict.b = 2  # This works, and uses the dict. Inefficient.

child_slots = ChildWithSlots()
child_slots.a = 1
child_slots.b = 2
# child_slots.c = 3  # This would fail with an AttributeError
  1. Multiple Inheritance is Often Broken: Multiple inheritance with __slots__ is a nightmare and frequently doesn’t work unless all parent classes have identical __slots__ definitions. Just avoid it. Seriously.

Best Practices and When to Use It

So, when does this pain become worth it?

  • You’re Creating a Lot of Instances: We’re talking thousands, preferably millions. It’s overkill for a class you instantiate a dozen times.
  • Your Class is a Data-Centric Workhorse: Think objects representing users, particles in a simulation, nodes in a graph, or financial transactions. These are perfect candidates.
  • You Want to Prevent Dynamic Assignment: Sometimes, the inability to create new attributes is a feature, not a bug. It can prevent typos (user.nmae = 'Gudio') from silently creating new, erroneous attributes.

Here’s a more realistic, useful example:

class DataPoint:
    """A simple data point for a high-volume time series."""
    __slots__ = ('timestamp', 'value', 'quality_flag')
    def __init__(self, timestamp, value, quality_flag):
        self.timestamp = timestamp
        self.value = value
        self.quality_flag = quality_flag

    def __repr__(self):
        return f"DataPoint(t={self.timestamp}, v={self.value})"

# Simulate reading 1,000,000 data points from a file
data_points = []
# ... in your real code, you'd be reading from a file or sensor
for i in range(1000000):
    data_points.append(DataPoint(i, i*0.5, i % 2))

print(f"Memory for 1,000,000 DataPoints: ~{sys.getsizeof(data_points) + sum(sys.getsizeof(p) for p in data_points)} bytes")

In summary, __slots__ is a powerful, specific tool. It’s not for everyday use, but when you hit that scaling problem, it’s one of the most effective ways to reduce your memory footprint without rewriting your entire application in another language. Just watch your step around the inheritance tree.