Python is a dynamic language, and you can add or subtract properties or methods to instances or classes dynamically. But using the __slots__ attribute can qualify class or instance properties and methods; without __slots__ the instance properties and methods are contained in the instance’s __dict__ dictionary, and the class properties and methods are contained in the class’s __dict__ dictionary.

The following problems may occur when using __slots__ as written in the normal way.

  1. AttributeError: ‘Xxx’ object has no attribute ‘yyy’
  2. AttributeError: ‘Xxx’ object attribute ‘yyy’ is read-only
  3. ValueError: ‘yyy’ in __slots__ conflicts with class variable

Let’s look at the following example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
>>> class Cat:
...     lags = 4
...     def __init__(self):
...         self.eyes = 2
...
...     def walk(self):
...         pass
...
...
>>> Cat.__dict__
mappingproxy({'__module__': '__main__', 'lags': 4, '__init__': <function Cat.__init__ at 0x106816700>, 'walk': <function Cat.walk at 0x106817920>, '__dict__': <attribute
'__dict__' of 'Cat' objects>, '__weakref__': <attribute '__weakref__' of 'Cat' objects>, '__doc__': None})
>>> c1 = Cat()
>>> c1.__dict__
{'eyes': 2}
>>> c1.miaow = lambda: 'hello'
>>> c1.__dict__
{'eyes': 2, 'miaow': <function <lambda> at 0x106d6fce0>}
>>> Cat.ears = 2
>>> c1.ears
2
>>> c1.__dict__
{'eyes': 2, 'miaow': <function <lambda> at 0x106d6fce0>}
>>> Cat.__dict__
mappingproxy({'__module__': '__main__', 'lags': 4, '__init__': <function Cat.__init__ at 0x106816700>, 'walk': <function Cat.walk at 0x106817920>, '__dict__': <attribute
'__dict__' of 'Cat' objects>, '__weakref__': <attribute '__weakref__' of 'Cat' objects>, '__doc__': None, '__getattribute__': <slot wrapper '__getattribute__' of 'object'
 objects>, 'ears': 2})

Classes or instances can add properties and methods at will.

If we introduce __slots__ to qualify properties or methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
>>> class Cat:
...     __slots__ = ('lags', 'eyes')
...     def __init__(self):
...         self.eyes = 2
...
...
...
>>> Cat.__dict__
mappingproxy({'__module__': '__main__', '__slots__': ('lags', 'eyes'), '__init__': <function Cat.__init__ at 0x10699ef20>, 'eyes': <member 'eyes' of 'Cat' objects>, 'lags
': <member 'lags' of 'Cat' objects>, '__doc__': None})
>>> c1 = Cat()
>>> c1.__dict__
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    c1.__dict__
AttributeError: 'Cat' object has no attribute '__dict__'
>>> c1.lags
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    c1.lags
AttributeError: 'Cat' object has no attribute 'lags'
>>> c1.eyes
2
>>> c1.lags = 4
>>> c1.ears = 2
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    c1.ears = 2
     ^^^^^^^
AttributeError: 'Cat' object has no attribute 'ears'
>>> c1.miaow = lambda: 'hello'
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    c1.miaow = lambda: 'hello'
     ^^^^^^^^
AttributeError: 'Cat' object has no attribute 'miaow'
>>> c1.lags = lambda: 'hello'
>>> c1.lags
<function <lambda> at 0x106d6c7c0>

With the introduction of __slots__, instances no longer have the __dict__ attribute and can only add attributes or methods listed in __slots__. Adding a property or method that is not listed in __slots__ will result in an error.

1
AttributeError: 'Xxx' object has no attribute 'yyy'

The same is true in the initialization function __init__(self), as in

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
>>> class Cat:
...     __slots__ = ('lags', 'eyes')
...     def __init__(self):
...         self.ears = 2
...
...
...
>>> c1 = Cat()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    c1 = Cat()
          ^^^^^
  File "<input>", line 4, in __init__
    self.ears = 2
    ^^^^^^^^^
AttributeError: 'Cat' object has no attribute 'ears'

There are no ears in __slots__, so you cannot add this property inside the initialization method or dynamically.

But the methods declared when defining the class are not bound by __slots__.

1
2
3
4
5
6
7
8
>>> class Cat:
...     __slots__ = ('lags', 'eyes')
...     def walk(self):
...         pass
...
...
>>> c1 = Cat()
>>> c1.walk()

__slots__ also does not constrain the dynamic addition of properties or methods via classes.

1
2
3
4
5
6
>>> class Cat:
...     __slots__ = ('lags', 'eyes')
...
...
>>> Cat.ears = 2
>>>

Class variables cannot be included in __slots__, for example

1
2
3
4
5
6
7
8
9
>>> class Cat:
...     __slots__ = ('lags')
...     lags = 4
...
...
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    class Cat:
ValueError: 'lags' in __slots__ conflicts with class variable

Declared class variables that are not defined in __slots__ are read-only for instance methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
>>> class Cat:
...     __slots__ = ('lags')
...     eyes = 2
...     def __init__(self):
...         self.eyes = 3
...
...
...
>>> c1 = Cat()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    c1 = Cat()
          ^^^^^
  File "<input>", line 5, in __init__
    self.eyes = 3
    ^^^^^^^^^
AttributeError: 'Cat' object attribute 'eyes' is read-only

However, class variables declared in classes that are not defined in __slots__ cannot be modified by instances, but can be modified by class attributes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
>>> class Cat:
...     __slots__ = ('lags')
...     eyes = 2
...
...
>>> c1 = Cat()
>>> c1.eyes = 3
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    c1.eyes = 3
     ^^^^^^^
AttributeError: 'Cat' object attribute 'eyes' is read-only
>>> Cat.eyes = 3
>>> c1.eyes
3

Properties or methods defined in __slots__ will have smart hints in the IDE.

__slots__ only works in the current class and does not affect subclasses, which need to define their own __slots__.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> class Animal:
...     __slots__ = ('lags')
...
...
>>> class Cat(Animal):
...     pass
...
>>> Animal.__slots__
'lags'
>>> Cat.__slots__
'lags'
>>> c = Cat()
>>> c.eyes = 2

__slots__ is read-only for instances, and can be modified via the class’s __slots__ attribute without changing the original constraints.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> class Cat:
...     __slots__ = ('lags', 'eyes')
...
...
>>> c.__slots__ = ('lags', 'eyes', 'ears')
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    c.__slots__ = ('lags', 'eyes', 'ears')
     ^^^^^^^^^^^
AttributeError: 'Cat' object attribute '__slots__' is read-only
>>> Cat.__slots__ = ('lags', 'eyes', 'ears')
>>> c = Cat()
>>> c.ears = 2
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    c.ears = 2
     ^^^^^^
AttributeError: 'Cat' object has no attribute 'ears'
>>> c.__slots__
('lags', 'eyes', 'ears')
>>> Cat.__slots__
('lags', 'eyes', 'ears')

__slots__ can also be declared as a list, or as a tuple omitting the parentheses.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

>>> class Cat:
...     __slots__ = ['lags', 'eyes']
...
...
>>> class Cat:
...     __slots__ = 'lags', 'eyes'
...
...
>>> type(Cat.__slots__)
<class 'tuple'>

Also, using __slots__ saves some memory by avoiding the use of __dict__ to record instance properties and methods.