__slots__ & Memory Optimization

Reviewed & published by Brayan K

__slots__ is a class declaration that lists exactly which attributes instances may have, letting Python drop the per-instance __dict__ and store those attributes in fixed slots — making each object smaller and attribute access a touch faster.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

When you create millions of small objects, the hidden __dict__ on every instance dominates your memory use. __slots__ trades a little flexibility for substantial savings — the right tool when you have many instances and few attributes.

What You'll Learn in This Lesson

1 Every Instance Has a Hidden __dict__

By default, Python stores an object's attributes in a dictionary attached to each instance — __dict__. It's flexible (you can add any attribute, anytime) but every instance pays the cost of that dictionary:

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.color = "red"            # totally allowed — dict grows
print(p.__dict__)          # {'x': 1, 'y': 2, 'color': 'red'}

📐 Where attributes live by default:

p.x   ->   p.__dict__["x"]      (a dictionary lookup)

That dictionary is convenient, but for one or two attributes it's a lot of overhead — and it repeats for every single instance you create.

🎯 Declaring __slots__ to Drop the __dict__

Add a class-level __slots__ listing your attribute names and Python reserves fixed storage for exactly those — and removes __dict__ entirely:

class Point:
    __slots__ = ("x", "y")        # only these attributes allowed

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

p = Point(1, 2)
print(p.x, p.y)                   # 1 2  (works exactly as before)

print(hasattr(p, "__dict__"))     # False  <- no per-instance dict!

The class behaves the same from the outside — you still write p.x — but each instance is now leaner because there's no dictionary riding along. The attributes live in compact, fixed slots instead.

2 The Restriction: No New Attributes

Here's the trade-off. Without a __dict__, you can't tack on attributes that aren't in __slots__. Trying to do so raises AttributeError:

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

p = Point(1, 2)
p.x = 10              # fine — 'x' is a declared slot

try:
    p.color = "red"  # NOT declared
except AttributeError as e:
    print("Error:", e)
# Error: 'Point' object has no attribute 'color'
class Particle:
    __slots__ = ("x", "y", "vx", "vy")

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

    def step(self):
        self.x += self.vx
        self.y += self.vy

p = Particle(0, 0, 2, 3)
p.step()
p.step()
print("Position:", p.x, p.y)
print("Has __dict__?", hasattr(p, "__dict__"))

# Adding an undeclared attribute is rejected
try:
    p.mass = 5
except AttributeError as e:
    print("Rejected:", e)

# ✅ Expected output:
# Position: 4 6
# Has __dict__? False
# Rejected: 'Particle' object has no attribute 'mass'

3 Measuring the Savings with sys.getsizeof

sys.getsizeof reports an object's size in bytes. A regular instance is small on its own, but it drags along a separate __dict__ — so the true cost is the instance plus its dictionary. A slotted instance has no such dictionary:

import sys

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

class WithSlots:
    __slots__ = ("x", "y", "z")
    def __init__(self, x, y, z):
        self.x, self.y, self.z = x, y, z

a = WithDict(1, 2, 3)
b = WithSlots(1, 2, 3)

# The dict version's real cost includes its __dict__
dict_total = sys.getsizeof(a) + sys.getsizeof(a.__dict__)
slots_total = sys.getsizeof(b)

print("dict has __dict__: ", hasattr(a, "__dict__"))   # True
print("slots has __dict__:", hasattr(b, "__dict__"))   # False
print("slots smaller:     ", slots_total < dict_total) # True

⚡ When It Matters & Slotted Dataclasses

It matters at scale

One object: irrelevant. A million objects: the __dict__ overhead can be hundreds of megabytes. Slots shine for data-heavy structures — graph nodes, simulation particles, parsed records:

class Node:
    __slots__ = ("value", "next")
    def __init__(self, value):
        self.value = value
        self.next = None

# Build a linked list of 5 slotted nodes
head = Node(0)
cur = head
for i in range(1, 5):
    cur.next = Node(i)
    cur = cur.next

# Walk it
node, out = head, []
while node:
    out.append(node.value)
    node = node.next
print(out)               # [0, 1, 2, 3, 4]

The clean modern way: @dataclass(slots=True)

Python 3.10+ generates the slots for you when you ask a dataclass for them:

from dataclasses import dataclass

@dataclass(slots=True)
class Pixel:
    x: int
    y: int
    color: str

px = Pixel(3, 4, "red")
print(px)                       # Pixel(x=3, y=4, color='red')
print(hasattr(px, "__dict__"))  # False  (slotted automatically)

🎯 Your Turn: Fill in the Blank

Complete the class so it stores only lat and lon with no __dict__. Replace each ___, then run it.

# 🎯 YOUR TURN: replace each ___
class Coord:
    ___ = ("lat", "lon")     # the declaration that drops __dict__

    def __init__(self, lat, lon):
        self.lat = lat
        self.___ = lon        # store the second slot

c = Coord(40.7, -74.0)
print(c.lat, c.lon)
print("Has __dict__?", hasattr(c, "___"))   # check for the dict

# Expected output:
# 40.7 -74.0
# Has __dict__? False
class Coord:
    __slots__ = ("lat", "lon")

    def __init__(self, lat, lon):
        self.lat = lat
        self.lon = lon

c = Coord(40.7, -74.0)
print(c.lat, c.lon)                       # 40.7 -74.0
print("Has __dict__?", hasattr(c, "__dict__"))  # False

Common Errors

❌ A single string instead of a tuple of names

__slots__ = "x"   # Python iterates the STRING -> one slot named 'x'
                  # but multi-letter names break confusingly

✅ Always use a tuple (or list) of strings, even for one slot:

__slots__ = ("x",)   # note the trailing comma

❌ Assigning an undeclared attribute

p.timestamp = 123   # AttributeError if 'timestamp' isn't a slot

✅ Add it to __slots__, or if you truly need dynamic attributes, include "__dict__" in the slots (which gives back the dictionary and most of the savings are lost).

❌ Expecting cached_property to work with slots

@cached_property      # needs __dict__ to store the cache
def total(self): ...  # raises TypeError on a slotted class

✅ Slots remove __dict__, which cached_property relies on. Use a plain @property, or don't slot that class.

🎯 Mini Challenge: Slotted vs Dict

Build a slotted Vec2 with x and y and a length method, prove it has no __dict__, and confirm an undeclared attribute is rejected.

import math

class Vec2:
    __slots__ = ("x", "y")

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

    def length(self):
        return math.hypot(self.x, self.y)

v = Vec2(3, 4)
print("Length:", v.length())
print("Has __dict__?", hasattr(v, "__dict__"))

try:
    v.z = 10
except AttributeError as e:
    print("Rejected:", e)

# ✅ Expected output:
# Length: 5.0
# Has __dict__? False
# Rejected: 'Vec2' object has no attribute 'z'

📚 Keep Learning

Frequently Asked Questions

Lesson complete — you can optimize memory now!

You understand the per-instance __dict__, how __slots__ drops it for smaller, faster objects, the no-new-attributes restriction, how to measure with sys.getsizeof, and when slots are worth it. Use them deliberately, at scale.

Practice quiz

What does declaring __slots__ on a class do to its instances?

  • It removes the per-instance __dict__ and stores attributes in fixed slots
  • It makes every attribute read-only
  • It automatically adds a __dict__ to each instance
  • It speeds up class creation but not attribute access

Answer: It removes the per-instance __dict__ and stores attributes in fixed slots. __slots__ reserves fixed storage for the listed attributes and drops the per-instance __dict__, making instances smaller.

After class Point: __slots__ = ('x','y'), what is hasattr(Point(1,2), '__dict__')?

  • True
  • False
  • It raises AttributeError
  • None

Answer: False. A fully slotted class has no per-instance __dict__, so hasattr returns False.

What happens when you assign an attribute that is NOT listed in __slots__?

  • It is silently added to a hidden __dict__
  • It works but uses extra memory
  • It raises AttributeError
  • It raises TypeError

Answer: It raises AttributeError. Without a __dict__, assigning an undeclared attribute raises AttributeError — the restriction is the whole point.

Why is __slots__ described as a memory optimization?

  • It compresses attribute values
  • It eliminates the per-instance dictionary, which matters across many instances
  • It shares attributes between all instances
  • It moves attributes to disk

Answer: It eliminates the per-instance dictionary, which matters across many instances. Dropping the __dict__ on every instance saves memory that multiplies across millions of small objects.

Which is the correct way to declare a single slot?

  • __slots__ = 'x'
  • __slots__ = ('x',)
  • __slots__ = ['x':]
  • __slots__ = {'x'}

Answer: __slots__ = ('x',). Use a tuple (note the trailing comma). A bare string is iterated character by character, which breaks for multi-letter names.

Since which Python version can a dataclass generate slots with @dataclass(slots=True)?

  • 3.6
  • 3.8
  • 3.10
  • 2.7

Answer: 3.10. @dataclass(slots=True) was added in Python 3.10, generating a slotted class automatically.

Which function reports an object's size in bytes for measuring savings?

  • len()
  • sys.getsizeof()
  • id()
  • sys.getrefcount()

Answer: sys.getsizeof(). sys.getsizeof reports an object's size in bytes; a slotted instance has no separate __dict__ to add to the total.

Why does @cached_property typically fail on a slotted class?

  • cached_property is deprecated
  • It needs a writable __dict__ to store the cache, which slots remove
  • It only works on functions, not methods
  • Slots make all attributes immutable

Answer: It needs a writable __dict__ to store the cache, which slots remove. cached_property stores its cached value in the instance __dict__, which a fully slotted class does not have.

When is __slots__ MOST worth using?

  • When you have a handful of objects with many attributes
  • When you create a very large number of small instances with a fixed set of attributes
  • Whenever you define any class, as a habit
  • Only for classes that need dynamic attributes

Answer: When you create a very large number of small instances with a fixed set of attributes. Slots shine at scale: many instances and few, fixed attributes. For a few objects the savings are irrelevant.

How can you keep a __dict__ AND use __slots__ if you truly need dynamic attributes?

  • Include '__dict__' in the __slots__ tuple
  • It is impossible to combine them
  • Set __slots__ = None
  • Use @property on every attribute

Answer: Include '__dict__' in the __slots__ tuple. Adding '__dict__' to __slots__ gives back the dictionary, but most of the memory savings are then lost.

Continue this course

Frequently asked questions

What does __slots__ actually do?

Normally every instance stores its attributes in a per-instance dictionary called __dict__, which is flexible but memory-heavy. Declaring __slots__ = ('x', 'y') tells Python to reserve fixed storage slots for exactly those attributes and skip the __dict__ entirely. Instances become smaller and attribute access becomes a little faster.

When is __slots__ worth using?

When you create a very large number of small instances — millions of points, records, or nodes — the per-instance __dict__ overhead adds up fast, and __slots__ can cut memory use substantially. For a handful of objects the savings are irrelevant, so don't reach for it by default; it's a targeted optimization, not a habit.

What's the catch with __slots__?

You lose the ability to add new attributes that weren't declared — assigning an unlisted attribute raises AttributeError. You also lose __dict__ (unless you add it back), and there are interactions with inheritance, multiple inheritance, and tools like cached_property that rely on a writable __dict__. It trades flexibility for efficiency.

Do dataclasses support __slots__?

Yes. Since Python 3.10 you can write @dataclass(slots=True) and the decorator generates a slotted class for you, combining the convenience of dataclasses with the memory savings of __slots__. It's often the cleanest way to get both.