Introduction

While reading the python data model documentation, I came across something I hadn't seen before. __slots__ is an optional argument that allows users to "explicitly declare data members". It is an interesting concept that I haven't seen utilized, but perhaps the reason is that not many people are aware it exists. I am going to explore this attribute that is available to see if it might provide value for my future projects. According to this blog post, __slots__ can significantly reduce the amount of ram required to create objects (40-50%!). Now let's dive in and figure out how it's used!

Python Version Check:
3.8.8 (default, Apr 13 2021, 19:58:26) 
[GCC 7.3.0]

Example #1: Typical Case

The first example we look at is the working example. we will have a class A1 with slots set to accept one var named var1.

class A1:
    __slots__ = ['var1']
    def __init__(self, value_passed_through_here):
        self.var1 = value_passed_through_here
a1 = A1(1); a1.var1
1

a has been created and everything is going well. Let's try adding another attribute.

a1.var2 = "but can I set another var?"
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-6-468e53fb51e0> in <module>
----> 1 a1.var2 = "but can I set another var?"

AttributeError: 'A1' object has no attribute 'var2'

a.var2 fails as expected because it isn't in the __slots__ list and __slots__ is read-only so it cannot be updated.

a1.__slots__ = ['var2']
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-7-5213b9df3654> in <module>
----> 1 a1.__slots__ = ['var2']

AttributeError: 'A1' object attribute '__slots__' is read-only
a1.__slots__ = ['var1', 'var2']
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-8-47cd4105f78c> in <module>
----> 1 a1.__slots__ = ['var1', 'var2']

AttributeError: 'A1' object attribute '__slots__' is read-only

When __slots__ is used, the __dict__ value is not set. Let's explore that a little further though.

a1.__dict__
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-9-79636e85e82f> in <module>
----> 1 a1.__dict__

AttributeError: 'A1' object has no attribute '__dict__'

Example #2a: Exploring __dict__ Without Using __slots__

class A2A:
    def __init__(self, value_passed_through_here):
        self.var1 = value_passed_through_here
a2a = A2A(1)

var1 shows up as expected when creating an object

a2a.__dict__
{'var1': 1}
a2a.var2 = 'adding a second thing'

Adding a second variable adds it to the __dict__ as expected

a2a.__dict__
{'var1': 1, 'var2': 'adding a second thing'}

Example #2b: Exploring __dict__ When Using __slots__

class A2B:
    __slots__ = ['var1', '__dict__']
    def __init__(self, value_passed_through_here):
        self.var1 = value_passed_through_here
a2b = A2B(1)
a2b.__dict__
{}

__dict__ exists now since we added it to __slots__, but it isn't populating the __dict__ like normal. We are still able to call the attribute var1 though.

a2b.var1
1
a2b.var2 = "test if we can add new variables now"

Surprisingly, once we add __dict__ to the __slots__ list, adding a new var works.

a2b.__dict__
{'var2': 'test if we can add new variables now'}

When we look at __dict__ after adding var2, there is an entry in __dict__ as well.

a2b.var2
'test if we can add new variables now'

So if we enable __dict__ we are able to add new items to the __dict__, but __dict__ has to be explicitly defined to work.

Example 3: Inheritance

Now that we've explored __slots__, let's see how it behaves when one class is inherited from another.

class A3:
    __slots__ = ['a']
    def __init__(self, a):
        self.a = a
a3 = A3(1); a3.a
1
class B3A(A3):
    def __init__(self):
        self.a = 1
        self.b = 2
b3a = B3A()
b3a.__slots__
['a']
b3a.__dict__
{'b': 2}
b3a.a
1
b3a.c = "can I set c?"
b3a.__dict__
{'b': 2, 'c': 'can I set c?'}

So when B3A is inherited from A3, it uses the slots class, but it also reverts back in a lot of ways to a normal, non-slots, class again. The last thing I'm going to try is actually setting a __slots__ in B3B just to see what happens

class B3B(A3):
    __slots__ = ['a','b']
    def __init__(self):
        self.a = 1
        self.b = 2
b3b = B3B()
b3b.c = "can I set this?"
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-33-032b9cd26579> in <module>
----> 1 b3b.c = "can I set this?"

AttributeError: 'B3B' object has no attribute 'c'

So now that we have given B3B a __slots__ it is no longer behaves the same way that B3A is.

Here is what the official documentation says about this:

The action of a __slots__ declaration is not limited to the class where it is defined. __slots__ declared in parents are available in child classes. However, child subclasses will get a __dict__ and __weakref__ unless they also define __slots__ (which should only contain names of any additional slots). (https://docs.python.org/3.8/reference/datamodel.html#notes-on-using-slots 5th bullet)

Conclusion

__slots__ is an interesting concept that is built into Python that I hadn't heard of and wanted to explore. Hopefully this notebook is informative to other Python users as well. Things didn't always behave as I would have expected and that's part of the fun of actually testing out the code to see how things work in practice.