Swiftorial Logo
Home
Swift Lessons
Tutorials
Learn More
Career
Resources

Python FAQ: Top Questions

33. What is the purpose of `__slots__` in Python classes?

The `__slots__` attribute in Python classes is a mechanism to explicitly declare instance attributes and prevent the creation of `__dict__` for instances. By default, instances of a Python class store their attributes in a dictionary called `__dict__`. While convenient, this dictionary consumes memory, especially for a large number of instances.

When `__slots__` is defined in a class, Python uses a more compact internal structure to store instance attributes instead of a dictionary. This can lead to significant memory savings and, in some cases, slightly faster attribute access.

How `__slots__` Works:

When you define `__slots__` in a class, you provide a tuple or list of strings, where each string is the name of an allowed instance attribute:


class MyClassWithSlots:
    __slots__ = ('attr1', 'attr2')

    def __init__(self, val1, val2):
        self.attr1 = val1
        self.attr2 = val2
        # self.attr3 = 'new' # This would raise AttributeError!
        
  • Instead of allocating a `__dict__` for each instance, Python allocates space for a fixed set of attributes defined in `__slots__`.
  • Each attribute defined in `__slots__` essentially becomes a descriptor, directly mapping to a fixed offset in the instance's memory layout.
  • If `__slots__` is defined, instances **cannot** have arbitrary new attributes added to them dynamically unless `__dict__` is explicitly included in `__slots__` (which defeats the memory-saving purpose).
  • Subclasses inherit `__slots__`. If a subclass also defines `__slots__`, its `__slots__` are added to its parent's `__slots__`.

Purpose and Benefits:

  1. Memory Savings (Primary Reason):
    • This is the most significant benefit. For applications that create a very large number of small objects (e.g., in data processing, games, or scientific simulations), using `__slots__` can reduce the memory footprint by 20-50% or more, as it avoids the overhead of a `__dict__` for each instance.
    • A `__dict__` can take up a considerable amount of memory (e.g., 200-300 bytes per instance, even if empty, plus space for keys and values).
  2. Faster Attribute Access (Secondary Benefit):
    • Accessing attributes defined in `__slots__` can be slightly faster than dictionary lookups, as the interpreter can directly access a fixed offset rather than performing a hash table lookup. This performance gain is usually negligible for most applications but can matter in very performance-critical loops.
  3. Prevents Arbitrary Attribute Creation:
    • By restricting which attributes can be assigned to an instance, `__slots__` can serve as a form of "data encapsulation" or schema enforcement. It helps prevent typos when assigning attribute names (e.g., `self.firs_name = "..."` instead of `self.first_name`). If you try to assign an attribute not in `__slots__`, an `AttributeError` is raised.

Drawbacks and Considerations:

  • Loss of `__dict__` and `weakref` capability: If `__slots__` is defined and `__dict__` is not included in it, instances cannot have a `__dict__`, meaning you cannot dynamically add new attributes to instances. They also cannot be weakly referenced unless `__weakref__` is explicitly added to `__slots__`.
  • More complex inheritance: Subclasses must also define `__slots__` if they want to retain the memory benefits and add new slot-based attributes. If a subclass does not define `__slots__`, it will default to having a `__dict__`.
  • Less flexible: Restricts the dynamic nature of Python objects, which might be undesirable if you rely on adding attributes at runtime.
  • Not for every class: The benefits are most pronounced for classes with a very large number of instances and a fixed, small set of attributes. For classes with few instances or those where dynamic attribute addition is common, `__slots__` can introduce more complexity than it solves.

In summary, `__slots__` is an optimization feature for memory-sensitive applications with many homogeneous objects. For typical applications, the default `__dict__` is perfectly fine and offers greater flexibility.


import sys

# --- Example 1: Class without __slots__ (default behavior) ---
print("--- Class without __slots__ ---")

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

p_regular = PointRegular(10, 20)

print(f"p_regular.x: {p_regular.x}")
print(f"p_regular.__dict__: {p_regular.__dict__}") # Has a __dict__

# Can add new attributes dynamically
p_regular.z = 30
print(f"p_regular.z (dynamically added): {p_regular.z}")
print(f"p_regular.__dict__ after adding z: {p_regular.__dict__}")

# Check memory usage (rough estimate for a single instance)
# Note: sys.getsizeof() reports memory taken by the object itself,
# not including referenced objects like the dict.
# For more accurate total memory, external tools or deeper introspection are needed.
# But presence/absence of __dict__ significantly impacts total memory.
memory_regular = sys.getsizeof(p_regular)
memory_dict = sys.getsizeof(p_regular.__dict__)
print(f"Memory (p_regular object itself): {memory_regular} bytes")
print(f"Memory (p_regular.__dict__): {memory_dict} bytes")
# Total memory for the instance is roughly memory_regular + memory_dict + memory for x, y, z keys/values


# --- Example 2: Class with __slots__ ---
print("\n--- Class with __slots__ ---")

class PointSlots:
    __slots__ = ('x', 'y') # Declare allowed attributes

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

p_slots = PointSlots(10, 20)

print(f"p_slots.x: {p_slots.x}")

# Try to access __dict__ (will raise AttributeError)
try:
    print(f"p_slots.__dict__: {p_slots.__dict__}")
except AttributeError as e:
    print(f"Accessing p_slots.__dict__ failed: {e}")
    print("This means __slots__ prevented its creation.")

# Try to add a new attribute not in __slots__ (will raise AttributeError)
try:
    p_slots.z = 30
except AttributeError as e:
    print(f"Adding p_slots.z failed: {e}")
    print("This means new attributes cannot be added dynamically.")

memory_slots = sys.getsizeof(p_slots)
print(f"Memory (p_slots object itself): {memory_slots} bytes")
# Compare memory_slots with memory_regular. memory_slots is typically smaller.


# --- Example 3: Performance Comparison (Large Number of Objects) ---
print("\n--- Performance Comparison (Large Number of Objects) ---")
# This comparison is more illustrative for the primary memory benefit.
# Actual timing differences for attribute access might be subtle.

NUM_OBJECTS = 1_000_000

print(f"Creating {NUM_OBJECTS} objects...")

# Without __slots__
start_time = time.time()
regular_points = [PointRegular(i, i*2) for i in range(NUM_OBJECTS)]
end_time = time.time()
print(f"Time to create {NUM_OBJECTS} PointRegular objects: {end_time - start_time:.4f} seconds")
# Approximate total memory for all objects (not precise, just a rough idea)
# This would require a memory profiler for accurate results across all objects.
# The internal CPython layout savings are substantial here.

# With __slots__
start_time = time.time()
slots_points = [PointSlots(i, i*2) for i in range(NUM_OBJECTS)]
end_time = time.time()
print(f"Time to create {NUM_OBJECTS} PointSlots objects: {end_time - start_time:.4f} seconds")


# --- Example 4: Inheritance with __slots__ ---
print("\n--- Inheritance with __slots__ ---")

class BaseWithSlots:
    __slots__ = ('a', 'b')

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

class DerivedWithSlots(BaseWithSlots):
    __slots__ = ('c',) # Adds 'c' to inherited slots 'a', 'b'

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

dws = DerivedWithSlots(1, 2, 3)
print(f"DerivedWithSlots attributes: a={dws.a}, b={dws.b}, c={dws.c}")

try:
    dws.d = 4 # Try to add new attribute
except AttributeError as e:
    print(f"Cannot add 'd' to DerivedWithSlots instance: {e}")

# If subclass does NOT define __slots__, it gets a __dict__
class DerivedWithoutSlots(BaseWithSlots):
    # No __slots__ defined here
    def __init__(self, a, b, c):
        super().__init__(a, b)
        self.c = c

dws_no_slots = DerivedWithoutSlots(1, 2, 3)
print(f"DerivedWithoutSlots has __dict__: {dws_no_slots.__dict__}")
dws_no_slots.d = 4 # Can add dynamically
print(f"DerivedWithoutSlots.d (dynamically added): {dws_no_slots.d}")
        

Explanation of the Example Code:

  • **Class without `__slots__` (`PointRegular`):**
    • Instances of `PointRegular` have a `__dict__` attribute, which is a dictionary storing all instance-specific attributes.
    • You can dynamically add new attributes (`p_regular.z = 30`) after the instance has been created.
    • `sys.getsizeof(p_regular.__dict__)` shows the memory overhead of this dictionary for each instance.
  • **Class with `__slots__` (`PointSlots`):**
    • `__slots__ = ('x', 'y')` explicitly declares that `x` and `y` are the only allowed instance attributes.
    • Attempting to access `p_slots.__dict__` raises an `AttributeError` because `__dict__` is not created.
    • Attempting to add a new attribute not in `__slots__` (`p_slots.z = 30`) also raises an `AttributeError`, enforcing the defined schema.
    • `sys.getsizeof(p_slots)` will typically show a smaller memory footprint for the instance itself compared to `PointRegular`, as it doesn't need to accommodate a dictionary.
  • **Performance Comparison:**
    • Creating a large number of `PointSlots` objects is generally faster and consumes significantly less memory than creating `PointRegular` objects, especially when the number of instances is in the millions. This is the primary use case for `__slots__`. (Note: direct memory measurement of all objects with `sys.getsizeof()` is tricky; this example focuses on creation time which is often correlated to memory management).
  • **Inheritance with `__slots__`:**
    • `DerivedWithSlots` inherits from `BaseWithSlots`. When `DerivedWithSlots` also defines `__slots__ = ('c',)`, its instances will have slots for `a`, `b` (from base), and `c` (from derived). They still won't have a `__dict__` and cannot have arbitrary new attributes added.
    • If a subclass (`DerivedWithoutSlots`) of a slotted class (`BaseWithSlots`) does *not* define `__slots__` itself, its instances will revert to having a `__dict__`. This means they will consume more memory and will allow dynamic attribute assignment, essentially losing the benefits of `__slots__` from the parent.

This example demonstrates how `__slots__` impacts memory, attribute access, and class flexibility, making it a valuable optimization for specific scenarios while highlighting its trade-offs.