__slots__ & Memory Optimization

intermediate __slots__ memory optimization __dict__ attribute-access

By default, every Python object stores its attributes in a dictionary (__dict__). That dictionary is flexible — we can add any attribute at any time — but it costs memory. __slots__ replaces that dictionary with a fixed set of attribute slots, saving memory and making attribute access faster.

How __dict__ Works (The Default)

Normally, each instance carries its own __dict__:

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

p = Point(1, 2)
print(p.__dict__)  # {'x': 1, 'y': 2}
p.z = 3           # we can add attributes on the fly
print(p.__dict__)  # {'x': 1, 'y': 2, 'z': 3}

That flexibility is great, but each __dict__ is a hash table. For a class with just two attributes, we’re paying the overhead of an entire dictionary per instance.

Enter __slots__

__slots__ tells Python “these are the ONLY attributes this class will ever have.” Python then uses a more compact internal structure instead of a dictionary.

class Point:
    __slots__ = ('x', 'y')  # fixed attribute set

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

p = Point(1, 2)
print(p.x)     # 1 — works normally
# p.z = 3      # AttributeError: 'Point' object has no attribute 'z'
# p.__dict__   # AttributeError: 'Point' object has no attribute '__dict__'

We lose the ability to add random attributes, but we gain memory savings and speed.

Memory Savings — Concrete Numbers

The savings are real and measurable. Let’s compare creating a million instances:

import sys

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

class SlottedPoint:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

r = RegularPoint(1, 2)
s = SlottedPoint(1, 2)
print(sys.getsizeof(r) + sys.getsizeof(r.__dict__))  # ~152 bytes
print(sys.getsizeof(s))                                # ~48 bytes

That’s roughly 3x less memory per instance. With a million objects, that’s the difference between ~150 MB and ~48 MB. The savings come from eliminating the per-instance hash table.

Faster Attribute Access

Because Python knows exactly where each slot is in memory (it’s a fixed offset, not a hash lookup), reading and writing slotted attributes is faster — roughly 10-20% faster in microbenchmarks.

This matters when we’re doing millions of attribute accesses in tight loops.

__slots__ with Inheritance

This is where it gets tricky. If a parent class has __slots__ and a child class doesn’t define __slots__, the child gets __dict__ back.

class Base:
    __slots__ = ('x',)

class Child(Base):  # no __slots__ defined
    pass

c = Child()
c.x = 1   # uses slot from Base
c.y = 2   # works — Child has __dict__ again

For slots to work through the whole chain, every class in the hierarchy needs __slots__.

class Base:
    __slots__ = ('x',)

class Child(Base):
    __slots__ = ('y',)  # only NEW attributes

c = Child()
c.x = 1   # slot from Base
c.y = 2   # slot from Child
# c.z = 3  # AttributeError — no __dict__

Important: don’t repeat parent slots in the child. Only list new attributes in the child’s __slots__.

Combining __slots__ and __dict__

If we want slots for common attributes but still want the flexibility to add extras, we can include '__dict__' in __slots__:

class Flexible:
    __slots__ = ('x', 'y', '__dict__')

    def __init__(self, x, y):
        self.x = x  # stored in slot (fast, compact)
        self.y = y  # stored in slot

f = Flexible(1, 2)
f.z = 3  # stored in __dict__ (flexible)

We get slot-speed for the common attributes and dict-flexibility for extras. But we still pay the __dict__ overhead when we use it.

__slots__ in Dataclasses

Since Python 3.10, dataclasses have a slots=True parameter that handles everything for us:

from dataclasses import dataclass

@dataclass(slots=True)
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
# p.z = 3  # AttributeError — slots in effect

This is the easiest way to get slotted classes. No manual __slots__ definition needed.

When to Use __slots__

Use it when:

  • We’re creating many instances of the same class (thousands or millions)
  • The class has a fixed set of attributes that won’t change
  • Memory or attribute access speed matters (data processing, game objects, ORM rows)

Avoid it when:

  • We need to add attributes dynamically (plugins, monkey-patching)
  • The class has very few instances (savings don’t matter)
  • We’re using multiple inheritance with non-slotted classes (gets messy)

Quick Gotchas

  • No __dict__ means no vars(obj) — we can’t introspect attributes the usual way.
  • No __weakref__ by default — add it to __slots__ if we need weak references.
  • Can’t use __slots__ with variable-length built-in types (like inheriting from str or list).

In simple language, __slots__ trades flexibility for efficiency. Instead of a per-instance dictionary, Python stores attributes in fixed-size slots — using less memory and accessing them faster. We should reach for it when creating lots of instances with a known set of attributes, and @dataclass(slots=True) makes it painless.