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:
-
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).
-
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.
-
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.