Creating Immutable Classes with __slots__

In Python, classes by default allow for dynamic creation of instance attributes, which provides flexibility but also comes with some overhead in terms of memory and performance. To make a class more memory-efficient and restrict the dynamic creation of instance attributes, Python provides the __slots__ mechanism.

Additionally, using __slots__ can make your class behave more like an immutable data structure, as it prevents adding new attributes that aren’t explicitly defined.

The __slots__ mechanism allows you to define a fixed set of attributes for your class, eliminating the need for a per-instance dictionary (__dict__) to store attributes, which is the default behavior in Python objects.

Benefits of Using __slots__

  1. Memory Efficiency: When you define __slots__, Python no longer needs to maintain an attribute dictionary (__dict__) for each instance, which reduces the memory footprint, especially when dealing with many instances.
  2. Performance Optimization: Access to the defined attributes can be faster since the attribute lookup is simplified by the fixed structure provided by __slots__.
  3. Restricting Dynamic Attribute Creation: By using __slots__, you prevent adding new attributes to instances, making the class “semi-immutable” (though not truly immutable as existing attributes can still be changed).
  4. Controlled Attribute Definition: Only the attributes listed in __slots__ can be created for instances of the class. If you try to assign an attribute that is not defined in __slots__, Python will raise an AttributeError.

Code Example

class Point:
    __slots__ = ['x', 'y']

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

# Example usage
p1 = Point(1, 2)
print(p1.x, p1.y)  # Outputs: 1 2

# Attempting to add a new attribute outside of __slots__
try:
    p1.z = 3  # This will raise an AttributeError
except AttributeError as e:
    print(e)  # Outputs: 'Point' object has no attribute 'z'

Code Explanation

  1. Defining __slots__: In the Point class, __slots__ = ['x', 'y'] is defined to restrict the class to only two attributes: x and y. This means each instance of Point will only have space allocated for x and y—no dynamic attributes can be added beyond these.
  2. Constructor and Usage: The constructor (__init__) initializes the attributes x and y using the provided values. You can access these attributes normally, like p1.x and p1.y.
  3. Error Handling: When you try to add an attribute (p1.z = 3) that is not listed in __slots__, Python raises an AttributeError. This is because the Point class does not allow any attributes beyond the ones defined in __slots__.

When to Use __slots__

  • Memory-Constrained Environments: If you are creating thousands or millions of instances of a class and memory usage is a concern, using __slots__ can reduce the memory footprint.
  • Performance-Critical Applications: Accessing attributes can be faster in classes that use __slots__ because it avoids dictionary lookups for instance attributes.
  • Restricting Attribute Creation: If you want to control which attributes can be added to an object (and prevent accidental creation of new attributes), __slots__ is a way to enforce that.

Limitations of __slots__

  1. No Dynamic Attribute Addition: As shown in the example, attributes not listed in __slots__ cannot be added dynamically, which may limit flexibility in certain scenarios.
  2. Inheritance Issues: If a class with __slots__ is inherited, the child class must define its own __slots__ or inherit from the parent properly. Otherwise, the benefits of using __slots__ can be negated.
  3. Incompatibility with Some Features: You cannot use __dict__ or __weakref__ by default in classes with __slots__, unless explicitly listed.