__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
- How the per-instance __dict__ stores attributes
- Declaring __slots__ to drop the __dict__
- The memory and speed gains __slots__ brings
- The restriction: no new, undeclared attributes
- Measuring instance size with sys.getsizeof
- When slots matter — and when they don't
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__? Falseclass 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__")) # FalseCommon 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
- __slots__ in the Data Model reference — the full rules, including inheritance and __dict__ interactions.
- sys.getsizeof — measuring object size — how to inspect memory use of individual objects.
- dataclasses — the slots=True option — generate slotted dataclasses automatically (3.10+).
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
- Previous: Properties & Descriptors
- Next: Scope: global, nonlocal & LEGB — Master name resolution and avoid the UnboundLocalError trap
- Quick reference: Python cheat sheet
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.