When your application creates thousands or millions of small objects (like game entities, data points, or cache entries), memory usage can explode due to Python's per-instance dictionary overhead. Using __slots__ restricts objects to a fixed set of attributes, dramatically reducing memory consumption.

Be careful: if any class in the hierarchy lacks __slots__, instances get a __dict__ and lose the memory benefits.

When to Use slots

slots_definition.py
"""Basic __slots__ definition"""

# Without __slots__
print("Without __slots__:")

class PersonNoSlots:
    def __init__(self, name, age):
        self.name = name
        self.age = age

person1 = 
print(f"Name: {person1.name}, Age: {person1.age}")
print(f"Has __dict__: {hasattr(person1, '__dict__')}")
print(f"__dict__: {person1.__dict__}")

# Can add dynamic attributes
person1.email = "alice@example.com"
print(f"Added email: {person1.email}")

# With __slots__
print("\nWith __slots__:")

class PersonWithSlots:
    __slots__ = ['name', 'age']
    
    def __init__(self, name, age):
        self.name = name
        self.age = age

person2 = PersonWithSlots("Bob", 25)
print(f"Name: {person2.name}, Age: {person2.age}")
print(f"Has __dict__: {hasattr(person2, '__dict__')}")

# Cannot add dynamic attributes
try:
    person2.email = "bob@example.com"
except AttributeError as e:
    print(f"Error adding email: {e}")

# Tuple __slots__
print("\nTuple __slots__:")

class Point:
    __slots__ = ('x', 'y')  # Can use tuple instead of list
    
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return f"Point({self.x}, {self.y})"

p = Point(10, 20)
print(f"Point: {p}")
print(f"x={p.x}, y={p.y}")

# Multiple attributes
print("\nMultiple attributes:")

class User:
    __slots__ = ['user_id', 'username', 'email', 'created_at']
    
    def __init__(self, user_id, username, email, created_at):
        self.user_id = user_id
        self.username = username
        self.email = email
        self.created_at = created_at
    
    def __repr__(self):
        return f"User(id={self.user_id}, username='{self.username}')"

user = User(1, "alice", "alice@example.com", "2024-01-01")
print(f"User: {user}")

# Read-only slots
print("\nRead-only slots:")

class Config:
    __slots__ = ['_host', '_port']
    
    def __init__(self, host, port):
        self._host = host
        self._port = port
    
    @property
    def host(self):
        return self._host
    
    @property
    def port(self):
        return self._port

config = Config("localhost", 8080)
print(f"Config: {config.host}:{config.port}")

# Cannot modify (no setter)
try:
    config.host = "0.0.0.0"
except AttributeError as e:
    print(f"Error: {e}")

# Empty __slots__
print("\nEmpty __slots__:")

class Immutable:
    __slots__ = []  # No instance attributes allowed
    
    VALUE = 42  # Class attribute is OK

im = Immutable()
print(f"Class value: {im.VALUE}")

try:
    im.x = 10
except AttributeError as e:
    print(f"Error: {e}")

# Checking __slots__
print("\nChecking __slots__:")

class Example:
    __slots__ = ['a', 'b', 'c']

print(f"__slots__: {Example.__slots__}")
print(f"Allowed attributes: {', '.join(Example.__slots__)}")

# Practical example
print("\nPractical example:")

class Vector2D:
    __slots__ = ['x', 'y']
    
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return f"Vector2D({self.x}, {self.y})"
    
    def __add__(self, other):
        return Vector2D(self.x + other.x, self.y + other.y)
    
    def magnitude(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5

v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)
v3 = v1 + v2

print(f"v1 = {v1}")
print(f"v2 = {v2}")
print(f"v3 = v1 + v2 = {v3}")
print(f"v1 magnitude = {v1.magnitude()}")

"""Basic __slots__ definition"""

# Without __slots__
print("Without __slots__:")

class PersonNoSlots:
    def __init__(self, name, age):
        self.name = name
        self.age = age

person1 = 
print(f"Name: {person1.name}, Age: {person1.age}")
print(f"Has __dict__: {hasattr(person1, '__dict__')}")
print(f"__dict__: {person1.__dict__}")

# Can add dynamic attributes
person1.email = "alice@example.com"
print(f"Added email: {person1.email}")

# With __slots__
print("\nWith __slots__:")

class PersonWithSlots:
    __slots__ = ['name', 'age']
    
    def __init__(self, name, age):
        self.name = name
        self.age = age

person2 = PersonWithSlots("Bob", 25)
print(f"Name: {person2.name}, Age: {person2.age}")
print(f"Has __dict__: {hasattr(person2, '__dict__')}")

# Cannot add dynamic attributes
try:
    person2.email = "bob@example.com"
except AttributeError as e:
    print(f"Error adding email: {e}")

# Tuple __slots__
print("\nTuple __slots__:")

class Point:
    __slots__ = ('x', 'y')  # Can use tuple instead of list
    
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return f"Point({self.x}, {self.y})"

p = Point(10, 20)
print(f"Point: {p}")
print(f"x={p.x}, y={p.y}")

# Multiple attributes
print("\nMultiple attributes:")

class User:
    __slots__ = ['user_id', 'username', 'email', 'created_at']
    
    def __init__(self, user_id, username, email, created_at):
        self.user_id = user_id
        self.username = username
        self.email = email
        self.created_at = created_at
    
    def __repr__(self):
        return f"User(id={self.user_id}, username='{self.username}')"

user = User(1, "alice", "alice@example.com", "2024-01-01")
print(f"User: {user}")

# Read-only slots
print("\nRead-only slots:")

class Config:
    __slots__ = ['_host', '_port']
    
    def __init__(self, host, port):
        self._host = host
        self._port = port
    
    @property
    def host(self):
        return self._host
    
    @property
    def port(self):
        return self._port

config = Config("localhost", 8080)
print(f"Config: {config.host}:{config.port}")

# Cannot modify (no setter)
try:
    config.host = "0.0.0.0"
except AttributeError as e:
    print(f"Error: {e}")

# Empty __slots__
print("\nEmpty __slots__:")

class Immutable:
    __slots__ = []  # No instance attributes allowed
    
    VALUE = 42  # Class attribute is OK

im = Immutable()
print(f"Class value: {im.VALUE}")

try:
    im.x = 10
except AttributeError as e:
    print(f"Error: {e}")

# Checking __slots__
print("\nChecking __slots__:")

class Example:
    __slots__ = ['a', 'b', 'c']

print(f"__slots__: {Example.__slots__}")
print(f"Allowed attributes: {', '.join(Example.__slots__)}")

# Practical example
print("\nPractical example:")

class Vector2D:
    __slots__ = ['x', 'y']
    
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return f"Vector2D({self.x}, {self.y})"
    
    def __add__(self, other):
        return Vector2D(self.x + other.x, self.y + other.y)
    
    def magnitude(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5

v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)
v3 = v1 + v2

print(f"v1 = {v1}")
print(f"v2 = {v2}")
print(f"v3 = v1 + v2 = {v3}")
print(f"v1 magnitude = {v1.magnitude()}")

"""Basic __slots__ definition"""

# Without __slots__
print("Without __slots__:")

class PersonNoSlots:
    def __init__(self, name, age):
        self.name = name
        self.age = age

person1 = 
print(f"Name: {person1.name}, Age: {person1.age}")
print(f"Has __dict__: {hasattr(person1, '__dict__')}")
print(f"__dict__: {person1.__dict__}")

# Can add dynamic attributes
person1.email = "alice@example.com"
print(f"Added email: {person1.email}")

# With __slots__
print("\nWith __slots__:")

class PersonWithSlots:
    __slots__ = ['name', 'age']
    
    def __init__(self, name, age):
        self.name = name
        self.age = age

person2 = PersonWithSlots("Bob", 25)
print(f"Name: {person2.name}, Age: {person2.age}")
print(f"Has __dict__: {hasattr(person2, '__dict__')}")

# Cannot add dynamic attributes
try:
    person2.email = "bob@example.com"
except AttributeError as e:
    print(f"Error adding email: {e}")

# Tuple __slots__
print("\nTuple __slots__:")

class Point:
    __slots__ = ('x', 'y')  # Can use tuple instead of list
    
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return f"Point({self.x}, {self.y})"

p = Point(10, 20)
print(f"Point: {p}")
print(f"x={p.x}, y={p.y}")

# Multiple attributes
print("\nMultiple attributes:")

class User:
    __slots__ = ['user_id', 'username', 'email', 'created_at']
    
    def __init__(self, user_id, username, email, created_at):
        self.user_id = user_id
        self.username = username
        self.email = email
        self.created_at = created_at
    
    def __repr__(self):
        return f"User(id={self.user_id}, username='{self.username}')"

user = User(1, "alice", "alice@example.com", "2024-01-01")
print(f"User: {user}")

# Read-only slots
print("\nRead-only slots:")

class Config:
    __slots__ = ['_host', '_port']
    
    def __init__(self, host, port):
        self._host = host
        self._port = port
    
    @property
    def host(self):
        return self._host
    
    @property
    def port(self):
        return self._port

config = Config("localhost", 8080)
print(f"Config: {config.host}:{config.port}")

# Cannot modify (no setter)
try:
    config.host = "0.0.0.0"
except AttributeError as e:
    print(f"Error: {e}")

# Empty __slots__
print("\nEmpty __slots__:")

class Immutable:
    __slots__ = []  # No instance attributes allowed
    
    VALUE = 42  # Class attribute is OK

im = Immutable()
print(f"Class value: {im.VALUE}")

try:
    im.x = 10
except AttributeError as e:
    print(f"Error: {e}")

# Checking __slots__
print("\nChecking __slots__:")

class Example:
    __slots__ = ['a', 'b', 'c']

print(f"__slots__: {Example.__slots__}")
print(f"Allowed attributes: {', '.join(Example.__slots__)}")

# Practical example
print("\nPractical example:")

class Vector2D:
    __slots__ = ['x', 'y']
    
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return f"Vector2D({self.x}, {self.y})"
    
    def __add__(self, other):
        return Vector2D(self.x + other.x, self.y + other.y)
    
    def magnitude(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5

v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)
v3 = v1 + v2

print(f"v1 = {v1}")
print(f"v2 = {v2}")
print(f"v3 = v1 + v2 = {v3}")
print(f"v1 magnitude = {v1.magnitude()}")

slots_memory_savings.py
"""Memory savings with __slots__"""

# Memory comparison
print("Memory comparison:")

import sys

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

class WithSlots:
    __slots__ = ['x', 'y', 'z']
    
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

# Create single instances
obj_no_slots = WithoutSlots(1, 2, 3)
obj_with_slots = WithSlots(1, 2, 3)

# Get sizes
size_no_slots = sys.getsizeof(obj_no_slots) + sys.getsizeof(obj_no_slots.__dict__)
size_with_slots = sys.getsizeof(obj_with_slots)

print(f"Without __slots__: {size_no_slots} bytes")
print(f"With __slots__: {size_with_slots} bytes")
print(f"Savings: {size_no_slots - size_with_slots} bytes ({100 * (1 - size_with_slots/size_no_slots):.1f}%)")

# Many instances
print("\nMany instances:")

# Create many instances
count = 100

no_slots_list = [WithoutSlots(i, i+1, i+2) for i in range(count)]
with_slots_list = [WithSlots(i, i+1, i+2) for i in range(count)]

# Estimate total size
total_no_slots = sum(sys.getsizeof(obj) + sys.getsizeof(obj.__dict__) for obj in no_slots_list[:100]) / 100 * count
total_with_slots = sum(sys.getsizeof(obj) for obj in with_slots_list[:100]) / 100 * count

print(f"{count} instances:")
print(f"Without __slots__: ~{total_no_slots/1024:.1f} KB")
print(f"With __slots__: ~{total_with_slots/1024:.1f} KB")
print(f"Estimated savings: ~{(total_no_slots - total_with_slots)/1024:.1f} KB")

# Simple data class
print("\nSimple data class:")

class Point:
    __slots__ = ['x', 'y']
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

# Create many points
points = [Point(i, i*2) for i in range(100)]

# Sample memory usage
sample_size = sys.getsizeof(points[0])
print(f"Point with __slots__: {sample_size} bytes each")
print(f"100 points: ~{sample_size * 100 / 1024:.1f} KB")

# Complex object
print("\nComplex object:")

class PersonNoSlots:
    def __init__(self, name, age, email, city):
        self.name = name
        self.age = age
        self.email = email
        self.city = city

class PersonWithSlots:
    __slots__ = ['name', 'age', 'email', 'city']
    
    def __init__(self, name, age, email, city):
        self.name = name
        self.age = age
        self.email = email
        self.city = city

p1 = PersonNoSlots("Alice", 30, "alice@example.com", "NYC")
p2 = PersonWithSlots("Alice", 30, "alice@example.com", "NYC")

size1 = sys.getsizeof(p1) + sys.getsizeof(p1.__dict__)
size2 = sys.getsizeof(p2)

print(f"PersonNoSlots: {size1} bytes")
print(f"PersonWithSlots: {size2} bytes")
print(f"Savings per instance: {size1 - size2} bytes")

# Large dataset
print("\nLarge dataset:")

class Record:
    __slots__ = ['id', 'timestamp', 'value', 'status']
    
    def __init__(self, id, timestamp, value, status):
        self.id = id
        self.timestamp = timestamp
        self.value = value
        self.status = status

# Simulate large dataset
dataset_size = 10000
print(f"Creating {dataset_size} records...")

# Estimate memory (based on sample)
sample = Record(1, 1234567890, 42.5, "active")
record_size = sys.getsizeof(sample)

print(f"Size per record: {record_size} bytes")
print(f"Total for {dataset_size} records: ~{record_size * dataset_size / 1024 / 1024:.1f} MB")

# Time series data
print("\nTime series data:")

class DataPoint:
    __slots__ = ['timestamp', 'value']
    
    def __init__(self, timestamp, value):
        self.timestamp = timestamp
        self.value = value

# Simulate 1 year of per-minute data
minutes_per_year = 365 * 24 * 60
point_size = sys.getsizeof(DataPoint(0, 0.0))

print(f"Data point size: {point_size} bytes")
print(f"1 year (minute resolution): ~{point_size * minutes_per_year / 1024 / 1024:.1f} MB")

# Practical comparison
print("\nPractical comparison:")

# Coordinates for game
class CoordNoSlots:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

class CoordWithSlots:
    __slots__ = ['x', 'y', 'z']
    
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

# Simulate game world with many entities
entity_count = 1000

# Sample sizes
no_slots = CoordNoSlots(0, 0, 0)
with_slots = CoordWithSlots(0, 0, 0)

size_no = sys.getsizeof(no_slots) + sys.getsizeof(no_slots.__dict__)
size_with = sys.getsizeof(with_slots)

print(f"{entity_count} game entities:")
print(f"Without __slots__: ~{size_no * entity_count / 1024:.1f} KB")
print(f"With __slots__: ~{size_with * entity_count / 1024:.1f} KB")
print(f"Memory saved: ~{(size_no - size_with) * entity_count / 1024:.1f} KB")

slots_no_dynamic_attrs.py
"""No dynamic attributes with __slots__"""

# Dynamic attributes blocked
print("Dynamic attributes blocked:")

class User:
    __slots__ = ['name', 'email']
    
    def __init__(self, name, email):
        self.name = name
        self.email = email

user = User("Alice", "alice@example.com")

# Allowed attributes work
print(f"Name: {user.name}")
print(f"Email: {user.email}")

# Modifying allowed attributes works
user.name = "Alice Smith"
print(f"Updated name: {user.name}")

# Adding new attribute fails
try:
    user.age = 30
except AttributeError as e:
    print(f"Error adding 'age': {e}")

try:
    user.city = "NYC"
except AttributeError as e:
    print(f"Error adding 'city': {e}")

# No __dict__
print("\nNo __dict__:")

class NoDict:
    __slots__ = ['x', 'y']
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

obj = NoDict(10, 20)

# __dict__ doesn't exist
print(f"Has __dict__: {hasattr(obj, '__dict__')}")

# Can't access __dict__
try:
    print(obj.__dict__)
except AttributeError as e:
    print(f"Error: {e}")

# __slots__ is available
print(f"__slots__: {obj.__slots__}")

# Deleting attributes
print("\nDeleting attributes:")

class Point:
    __slots__ = ['x', 'y']
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(5, 10)
print(f"Point: ({p.x}, {p.y})")

# Can delete slotted attributes
del p.x
print(f"Has x: {hasattr(p, 'x')}")

# Accessing deleted attribute fails
try:
    print(p.x)
except AttributeError as e:
    print(f"Error: {e}")

# Can reassign
p.x = 15
print(f"Reassigned x: {p.x}")

# Class attributes still work
print("\nClass attributes still work:")

class Config:
    __slots__ = ['name']
    
    # Class attributes are fine
    DEFAULT_TIMEOUT = 30
    VERSION = "1.0"
    
    def __init__(self, name):
        self.name = name

config = Config("prod")
print(f"Instance name: {config.name}")
print(f"Class DEFAULT_TIMEOUT: {config.DEFAULT_TIMEOUT}")
print(f"Class VERSION: {config.VERSION}")

# Can modify class attributes
Config.VERSION = "1.1"
print(f"Updated VERSION: {config.VERSION}")

# Methods still work
print("\nMethods still work:")

class Rectangle:
    __slots__ = ['width', 'height']
    
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height
    
    def perimeter(self):
        return 2 * (self.width + self.height)

rect = Rectangle(10, 5)
print(f"Area: {rect.area()}")
print(f"Perimeter: {rect.perimeter()}")

# Properties work
print("\nProperties work:")

class Temperature:
    __slots__ = ['_celsius']
    
    def __init__(self, celsius):
        self._celsius = celsius
    
    @property
    def celsius(self):
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        self._celsius = value
    
    @property
    def fahrenheit(self):
        return self._celsius * 9/5 + 32

temp = Temperature(25)
print(f"Celsius: {temp.celsius}")
print(f"Fahrenheit: {temp.fahrenheit}")

temp.celsius = 30
print(f"Updated - Celsius: {temp.celsius}, Fahrenheit: {temp.fahrenheit}")

# Weakref support
print("\nWeakref support:")

import weakref

# Without __weakref__ in __slots__, weakref fails
class NoWeakref:
    __slots__ = ['value']
    
    def __init__(self, value):
        self.value = value

obj1 = NoWeakref(42)

try:
    ref = weakref.ref(obj1)
except TypeError as e:
    print(f"Error without __weakref__: {e}")

# Add __weakref__ to support weak references
class WithWeakref:
    __slots__ = ['value', '__weakref__']
    
    def __init__(self, value):
        self.value = value

obj2 = WithWeakref(42)
ref = weakref.ref(obj2)
print(f"Weakref created: {ref()}")
print(f"Weakref value: {ref().value}")

# Pickle support
print("\nPickle support:")

import pickle

class PickleSlots:
    __slots__ = ['name', 'value']
    
    def __init__(self, name, value):
        self.name = name
        self.value = value

obj = PickleSlots("test", 123)

# Pickle and unpickle
pickled = pickle.dumps(obj)
restored = pickle.loads(pickled)

print(f"Original: {obj.name}, {obj.value}")
print(f"Restored: {restored.name}, {restored.value}")

# Practical example
print("\nPractical example:")

class ImmutablePoint:
    __slots__ = ['_x', '_y']
    
    def __init__(self, x, y):
        object.__setattr__(self, '_x', x)
        object.__setattr__(self, '_y', y)
    
    @property
    def x(self):
        return self._x
    
    @property
    def y(self):
        return self._y
    
    def __setattr__(self, name, value):
        raise AttributeError(f"Cannot modify immutable attribute '{name}'")
    
    def __repr__(self):
        return f"ImmutablePoint({self._x}, {self._y})"

p = ImmutablePoint(10, 20)
print(f"Point: {p}")
print(f"x={p.x}, y={p.y}")

# Cannot modify
try:
    p.x = 30
except AttributeError as e:
    print(f"Error: {e}")

# Cannot add attributes
try:
    p.z = 40
except AttributeError as e:
    print(f"Error: {e}")

slots_inheritance.py
"""__slots__ with inheritance"""

# Basic inheritance
print("Basic inheritance:")

class Base:
    __slots__ = ['x']
    
    def __init__(self, x):
        self.x = x

class Derived(Base):
    __slots__ = ['y']  # Only add new slots
    
    def __init__(self, x, y):
        super().__init__(x)
        self.y = y

obj = Derived(10, 20)
print(f"x={obj.x}, y={obj.y}")
print(f"Base __slots__: {Base.__slots__}")
print(f"Derived __slots__: {Derived.__slots__}")

# Combined slots are inherited
try:
    obj.z = 30
except AttributeError as e:
    print(f"Error: {e}")

# Empty slots in derived
print("\nEmpty slots in derived:")

class BaseClass:
    __slots__ = ['a', 'b']
    
    def __init__(self, a, b):
        self.a = a
        self.b = b

class DerivedClass(BaseClass):
    __slots__ = []  # No new slots
    
    def method(self):
        return self.a + self.b

obj = DerivedClass(5, 10)
print(f"a={obj.a}, b={obj.b}")
print(f"method()={obj.method()}")

# Parent without __slots__
print("\nParent without __slots__:")

class ParentNoSlots:
    def __init__(self, x):
        self.x = x

class ChildWithSlots(ParentNoSlots):
    __slots__ = ['y']
    
    def __init__(self, x, y):
        super().__init__(x)
        self.y = y

obj = ChildWithSlots(10, 20)

# Parent has __dict__, child has slots
print(f"Has __dict__: {hasattr(obj, '__dict__')}")
print(f"__dict__: {obj.__dict__}")  # Contains parent attributes

# Can add attributes to parent's __dict__
obj.z = 30
print(f"Added z: {obj.z}")

# But child's slotted attributes are separate
print(f"y (slotted): {obj.y}")

# Child without __slots__
print("\nChild without __slots__:")

class ParentWithSlots:
    __slots__ = ['x']
    
    def __init__(self, x):
        self.x = x

class ChildNoSlots(ParentWithSlots):
    # No __slots__ means __dict__ is created
    def __init__(self, x, y):
        super().__init__(x)
        self.y = y  # Stored in __dict__

obj = ChildNoSlots(10, 20)
print(f"x (slotted): {obj.x}")
print(f"y (dict): {obj.y}")
print(f"Has __dict__: {hasattr(obj, '__dict__')}")

# Can add dynamic attributes
obj.z = 30
print(f"Added z: {obj.z}")

# Multiple inheritance
print("\nMultiple inheritance:")

class A:
    __slots__ = ['a']

class B(A):
    __slots__ = ['b']

class C(B):
    __slots__ = ['c']

    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

obj = C(1, 2, 3)
print(f"a={obj.a}, b={obj.b}, c={obj.c}")

# Diamond inheritance
print("\nDiamond inheritance:")

class Base2:
    __slots__ = ['value']

class Left(Base2):
    __slots__ = ['left_value']

# For diamond to work, only one branch can have non-empty __slots__
class Right(Base2):
    __slots__ = []  # Empty slots - avoids layout conflict

class Diamond(Left, Right):
    __slots__ = ['diamond_value']

    def __init__(self, value, left, diamond):
        self.value = value
        self.left_value = left
        self.diamond_value = diamond

obj = Diamond(1, 2, 3)
print(f"value={obj.value}, left={obj.left_value}, diamond={obj.diamond_value}")

# Override with __dict__
print("\nOverride with __dict__:")

class Parent:
    __slots__ = ['x']

class Child(Parent):
    __slots__ = ['y', '__dict__']  # Add __dict__ explicitly
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

obj = Child(10, 20)
print(f"x={obj.x}, y={obj.y}")

# Can now add dynamic attributes
obj.z = 30
print(f"Added z: {obj.z}")
print(f"__dict__: {obj.__dict__}")

# Mixin classes
print("\nMixin classes:")

class SlottedBase:
    __slots__ = ['id', 'name']

class Mixin:
    def get_info(self):
        return f"{self.name} (ID: {self.id})"

class ConcreteClass(SlottedBase, Mixin):
    __slots__ = ['email']
    
    def __init__(self, id, name, email):
        self.id = id
        self.name = name
        self.email = email

obj = ConcreteClass(1, "Alice", "alice@example.com")
print(f"Info: {obj.get_info()}")
print(f"Email: {obj.email}")

# Practical example
print("\nPractical example:")

# Shape hierarchy
class Shape:
    __slots__ = ['color']
    
    def __init__(self, color):
        self.color = color

class Circle(Shape):
    __slots__ = ['radius']
    
    def __init__(self, color, radius):
        super().__init__(color)
        self.radius = radius
    
    def area(self):
        import math
        return math.pi * self.radius ** 2

class Rectangle(Shape):
    __slots__ = ['width', 'height']
    
    def __init__(self, color, width, height):
        super().__init__(color)
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height

circle = Circle("red", 5)
rectangle = Rectangle("blue", 10, 20)

print(f"Circle: color={circle.color}, radius={circle.radius}, area={circle.area():.2f}")
print(f"Rectangle: color={rectangle.color}, width={rectangle.width}, height={rectangle.height}, area={rectangle.area()}")

slots_use_cases.py
"""When to use __slots__"""

# When to use __slots__
print("When to use __slots__:")

# 1. Many instances
class Particle:
    __slots__ = ['x', 'y', 'vx', 'vy']
    
    def __init__(self, x, y, vx, vy):
        self.x = x
        self.y = y
        self.vx = vx
        self.vy = vy

# Useful when creating thousands/millions
particles = [Particle(i, i*2, i*3, i*4) for i in range(100)]
print(f"✓ Good: Created {len(particles)} particles with __slots__")

# Fixed attributes
print("\nFixed attributes:")

class DatabaseRecord:
    __slots__ = ['id', 'timestamp', 'user_id', 'action', 'data']
    
    def __init__(self, id, timestamp, user_id, action, data):
        self.id = id
        self.timestamp = timestamp
        self.user_id = user_id
        self.action = action
        self.data = data

record = DatabaseRecord(1, 1234567890, 42, "login", {"ip": "127.0.0.1"})
print(f"✓ Good: Fixed schema prevents typos")

# Data classes
print("\nData classes:")

class Point3D:
    __slots__ = ['x', 'y', 'z']
    
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
    
    def __repr__(self):
        return f"Point3D({self.x}, {self.y}, {self.z})"
    
    def distance_from_origin(self):
        return (self.x**2 + self.y**2 + self.z**2) ** 0.5

p = Point3D(3, 4, 5)
print(f"✓ Good: Simple data container")
print(f"  Point: {p}, distance: {p.distance_from_origin():.2f}")

# When NOT to use __slots__
print("\nWhen NOT to use __slots__:")

# 1. Dynamic attributes needed
class FlexibleConfig:
    # Don't use __slots__ here
    def __init__(self):
        self.settings = {}
    
    def set(self, key, value):
        self.settings[key] = value

config = FlexibleConfig()
config.debug = True  # Need this flexibility
config.timeout = 30
print(f"✗ Bad for __slots__: Dynamic attributes needed")

# Few instances
print("\nFew instances:")

# Don't use __slots__ for singletons or few instances
class AppConfig:
    # No need for __slots__ - only one instance
    def __init__(self):
        self.host = "localhost"
        self.port = 8080
        self.debug = False

config = AppConfig()
print(f"✗ Bad for __slots__: Only a few instances")

# Complex inheritance
print("\nComplex inheritance:")

# Avoid __slots__ with complex inheritance hierarchies
class Base:
    def __init__(self):
        self.base_attr = "base"

class Mixin1:
    def method1(self):
        return "mixin1"

class Mixin2:
    def method2(self):
        return "mixin2"

class ComplexClass(Base, Mixin1, Mixin2):
    # Don't add __slots__ here - too complex
    def __init__(self):
        super().__init__()
        self.extra = "extra"

obj = ComplexClass()
print(f"✗ Bad for __slots__: Complex inheritance")

# Performance-critical code
print("\nPerformance-critical code:")

class Vector:
    __slots__ = ['x', 'y']
    
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def dot(self, other):
        return self.x * other.x + self.y * other.y

# Good for tight loops
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(f"✓ Good: Performance-critical math operations")

# Large collections
print("\nLarge collections:")

class LogEntry:
    __slots__ = ['timestamp', 'level', 'message']
    
    def __init__(self, timestamp, level, message):
        self.timestamp = timestamp
        self.level = level
        self.message = message

# Good for storing millions of log entries in memory
logs = [LogEntry(i, "INFO", f"Message {i}") for i in range(100)]
print(f"✓ Good: Large collections ({len(logs)} entries)")

# Embedded systems
print("\nEmbedded systems:")

class SensorReading:
    __slots__ = ['sensor_id', 'value', 'unit']
    
    def __init__(self, sensor_id, value, unit):
        self.sensor_id = sensor_id
        self.value = value
        self.unit = unit

reading = SensorReading(1, 23.5, "C")
print(f"✓ Good: Memory-constrained environments")

# Game development
print("\nGame development:")

class Entity:
    __slots__ = ['id', 'x', 'y', 'sprite', 'health']
    
    def __init__(self, id, x, y, sprite, health):
        self.id = id
        self.x = x
        self.y = y
        self.sprite = sprite
        self.health = health

# Good for thousands of game entities
entities = [Entity(i, i*10, i*20, f"sprite{i}", 100) for i in range(100)]
print(f"✓ Good: Game with {len(entities)} entities")

# Practical decision guide
print("\nPractical decision guide:")

print("\nUse __slots__ when:")
print("  ✓ Creating many instances (>1000)")
print("  ✓ Attributes are fixed and known")
print("  ✓ Memory is constrained")
print("  ✓ Attribute access speed matters")
print("  ✓ Preventing attribute typos is valuable")

print("\nAvoid __slots__ when:")
print("  ✗ Dynamic attributes are needed")
print("  ✗ Only a few instances exist")
print("  ✗ Complex inheritance hierarchies")
print("  ✗ Need __dict__ for introspection")
print("  ✗ Using libraries that expect __dict__")

# Real-world example
print("\nReal-world example:")

# Time series data storage
class TimeSeriesPoint:
    __slots__ = ['timestamp', 'value']
    
    def __init__(self, timestamp, value):
        self.timestamp = timestamp
        self.value = value

# Simulate 1 day of per-second data
seconds_per_day = 86400
timeseries = [TimeSeriesPoint(i, i * 0.1) for i in range(100)]  # Sample

print(f"✓ Excellent use case: Time series with {len(timeseries)} data points")
print(f"  Each point: timestamp + value")
print(f"  Fixed schema, many instances, memory-efficient")

The decision comes down to: Do you have many instances? Are attributes fixed and known? Is memory or access speed a concern?

Benefits

  • Memory savings: ~40-50% reduction for simple classes
  • Faster attribute access: No dict lookup overhead
  • Type safety: Prevents typos in attribute names

Limitations

  • No dynamic attributes after instantiation
  • Inheritance requires care (all classes need __slots__)
  • No __dict__ by default (breaks some libraries)
  • Cannot use with multiple inheritance from slotted classes with non-empty slots
slots declaration Defining `__slots__` as a list or tuple of attribute names to specify the only attributes instances can have.
memory optimization The primary benefit of `__slots__` is reducing per-instance memory by ~40-50% for simple classes by eliminating the `__dict__` overhead.
attribute restriction With `__slots__`, attempting to add attributes not in the slots list raises an `AttributeError`, preventing typos and enforcing structure.
slots inheritance Child classes only need to define slots for their new attributes; they inherit parent slots automatically.
slots decision Use `__slots__` when creating many instances of simple classes; avoid it when you need dynamic attributes or have complex inheritance.

Exercise: slots_practice.py

Create a Point class with __slots__ and compare its memory usage to a regular class