__slots__ — Memory Optimization and Attribute Restriction
__slots__ pre-declares the attributes an instance can have, saving memory and speeding up attribute access.
__slots__ Basics
import sys
# Default class — uses __dict__
class PointWithDict:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
# Class using __slots__
class PointWithSlots:
__slots__ = ("x", "y")
def __init__(self, x: float, y: float):
self.x = x
self.y = y
p_dict = PointWithDict(1.0, 2.0)
p_slots = PointWithSlots(1.0, 2.0)
print(f"__dict__ version: {sys.getsizeof(p_dict) + sys.getsizeof(p_dict.__dict__)} bytes")
print(f"__slots__ version: {sys.getsizeof(p_slots)} bytes")
print(hasattr(p_dict, "__dict__")) # True
print(hasattr(p_slots, "__dict__")) # False
# Attempting to assign an undeclared attribute
try:
p_slots.z = 3.0 # AttributeError!
except AttributeError as e:
print(f"Error: {e}")
Memory Savings with Large Numbers of Instances
import tracemalloc
import sys
class LogEntryDict:
def __init__(self, timestamp: str, level: str, message: str):
self.timestamp = timestamp
self.level = level
self.message = message
class LogEntrySlots:
__slots__ = ("timestamp", "level", "message")
def __init__(self, timestamp: str, level: str, message: str):
self.timestamp = timestamp
self.level = level
self.message = message
N = 100_000
tracemalloc.start()
dict_entries = [
LogEntryDict("2024-01-15 10:00:00", "INFO", f"message {i}")
for i in range(N)
]
_, peak_dict = tracemalloc.get_traced_memory()
tracemalloc.stop()
del dict_entries
tracemalloc.start()
slots_entries = [
LogEntrySlots("2024-01-15 10:00:00", "INFO", f"message {i}")
for i in range(N)
]
_, peak_slots = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"__dict__ approach: {peak_dict / 1024 / 1024:.1f} MB")
print(f"__slots__ approach: {peak_slots / 1024 / 1024:.1f} MB")
print(f"Savings: {(1 - peak_slots / peak_dict) * 100:.1f}%")
Inheritance and __slots__
class Base:
__slots__ = ("x", "y")
def __init__(self, x: float, y: float):
self.x = x
self.y = y
class ChildWithSlots(Base):
"""Adds __slots__ — full optimization"""
__slots__ = ("z",)
def __init__(self, x: float, y: float, z: float):
super().__init__(x, y)
self.z = z
class ChildWithoutSlots(Base):
"""No __slots__ — __dict__ is created (reduced savings)"""
def __init__(self, x: float, y: float, name: str):
super().__init__(x, y)
self.name = name # This attribute is stored in __dict__
c1 = ChildWithSlots(1, 2, 3)
c2 = ChildWithoutSlots(1, 2, "test")
print(f"ChildWithSlots __slots__: {ChildWithSlots.__slots__}")
print(f"ChildWithoutSlots has __dict__: {hasattr(c2, '__dict__')}")
def get_all_slots(cls) -> list[str]:
slots = []
for klass in cls.__mro__:
slots.extend(getattr(klass, "__slots__", []))
return slots
print(f"ChildWithSlots all slots: {get_all_slots(ChildWithSlots)}")
dataclass with __slots__
from dataclasses import dataclass
# Python 3.10+: slots=True option for dataclass
@dataclass(slots=True)
class Pixel:
x: int
y: int
r: int = 0
g: int = 0
b: int = 0
a: int = 255
def to_tuple(self) -> tuple:
return (self.x, self.y, self.r, self.g, self.b, self.a)
import tracemalloc
WIDTH, HEIGHT = 100, 100
tracemalloc.start()
pixels = [Pixel(x, y, x % 256, y % 256, 0) for y in range(HEIGHT) for x in range(WIDTH)]
_, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"10,000 pixels (slots): {peak / 1024:.1f} KB")
__slots__ and __weakref__
import weakref
class SlottedNode:
__slots__ = ("value",) # No weakref support!
def __init__(self, value):
self.value = value
class SlottedNodeWithWeakref:
__slots__ = ("value", "__weakref__") # Explicitly include __weakref__
def __init__(self, value):
self.value = value
slotted = SlottedNode(42)
try:
ref_slotted = weakref.ref(slotted)
except TypeError as e:
print(f"Slotted node weakref error: {e}")
slotted_wr = SlottedNodeWithWeakref(42)
ref_wr = weakref.ref(slotted_wr)
print(f"Node with __weakref__: {ref_wr()}") # OK
Real-World Example: High-Volume Data Processing
from __future__ import annotations
import tracemalloc
import random
class Transaction:
"""Financial transaction record — __slots__ optimized for millions of records"""
__slots__ = (
"transaction_id", "timestamp", "amount",
"currency", "merchant", "category", "status",
)
def __init__(self, transaction_id: str, timestamp: str, amount: float,
currency: str, merchant: str, category: str, status: str = "completed"):
self.transaction_id = transaction_id
self.timestamp = timestamp
self.amount = amount
self.currency = currency
self.merchant = merchant
self.category = category
self.status = status
def __repr__(self) -> str:
return f"Transaction({self.transaction_id!r}, {self.amount} {self.currency})"
@property
def is_large(self) -> bool:
return self.amount > 1_000_000
def to_dict(self) -> dict:
return {slot: getattr(self, slot) for slot in self.__slots__}
categories = ["food", "transport", "shopping", "medical", "education"]
merchants = ["Starbucks", "Walmart", "Department Store", "Hospital", "School"]
tracemalloc.start()
txns = [
Transaction(
f"TXN{i:08d}", f"2024-01-{(i % 28) + 1:02d}",
round(random.uniform(10, 5000), 2), "USD",
random.choice(merchants), random.choice(categories),
)
for i in range(50_000)
]
_, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"50,000 transactions: {peak / 1024 / 1024:.1f} MB")
print(f"Per transaction: {peak / len(txns):.0f} bytes")
Pro Tips
1. Mixing __slots__ and __dict__
class FlexibleSlots:
"""Some attributes as slots, others in __dict__"""
__slots__ = ("x", "y", "__dict__") # Include __dict__ → allows extra attributes
def __init__(self, x: float, y: float):
self.x = x
self.y = y
p = FlexibleSlots(1.0, 2.0)
p.extra = "extra attribute" # Allowed because __dict__ is included
print(p.x, p.extra)
2. Combining properties with __slots__
class Temperature:
__slots__ = ("_celsius",)
def __init__(self, celsius: float):
self._celsius = celsius
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError(f"Below absolute zero: {value}")
self._celsius = value
@property
def fahrenheit(self) -> float:
return self._celsius * 9/5 + 32
@property
def kelvin(self) -> float:
return self._celsius + 273.15
t = Temperature(100)
print(f"Celsius: {t.celsius}°C")
print(f"Fahrenheit: {t.fahrenheit}°F")
print(f"Kelvin: {t.kelvin}K")
Summary
| Aspect | __dict__ | __slots__ |
|---|---|---|
| Memory usage | High (dictionary overhead) | Low (40–50% savings) |
| Attribute access | Hash lookup | Direct offset (faster) |
| Dynamic attributes | Allowed | Not allowed (by default) |
| weakref | Supported | Requires __weakref__ explicitly |
| When to use | General use | 10k+ instances, performance critical |
Use __slots__ when creating hundreds of thousands or more instances to simultaneously improve both memory and speed.