While writing the C++ default initialization post, I questioned myself if there have been changes in Python regarding to attribute declaration.

Sometimes you find constructs like the following:

class Foo:
    i:int = 1

Now lets look how it behaves:

>>> Foo.i
1
>>> p = Foo()
>>> p.i
1

So far, so good. But now lets change i:

>>> p = Foo()
>>> p.i
1
>>> Foo.i = 2
>>> p.i
2

For the old hands, this is no surprise. i is declared as a class member, so it belongs to the class Foo. As long as i is accessed at the instance level without assigning it, always Foo.i is returned.

If we take a deeper look at Foo and its instance p it will get clearer:

>>> Foo.__dict__
mappingproxy({'__module__': '__main__', '__annotations__': {'i': <class 'int'>}, 'i': 1, ...})
>>> p.__dict__
{}

So if you try to get a member from the instance p and this member is not defined for p, p will look if its class has such (only if __getattr__ or __getattribute__ is not explicitly defined)

Lets assign something to i at the instance p:

>>> p.i = 3
>>> p.i
3
>>> p.__dict__
{'i': 3}

Since the instance p didn’t already has an attribute i, it is added to p at this point. Its class Foo stays unchanged:

>>> Foo.i
2

So adding default values at classes seems to be a good idea, isn’t it?

No it isn’t - neither in terms of understandibility nor of maintainability!

For trivial types like int, float, bool, double etc. it works, even if the ‘how’ isn’t expected. So lets take another example, instead of an Integer we use a list:

>>> class Foo:
        l:list = []
>>> p = Foo()
>>> Foo.l
[]
>>> p.l
[]
>>> p.l += [1,2,3]
>>> p.l
[1,2,3]

But now the (not really) surprisingly behaviour:

>>> Foo.l
[1,2,3]

Since we didn’t assign l to p, p.l is still Foo.l.

If we now create another instance of Foo, and do the same thing, it will get even worse (as long it isn’t explicitly desired to act this way):

>>> p2 = Foo()
>>> p2.l
[1,2,3]
>>> p2.l += [4,5,6]
>>> p.l
[1,2,3,4,5,6]

So always define instance members inside the constructor:

class Foo:
    def __init__(self):
        self.l:list = []

Now l is an instance attribute and doesn’t have side effects:

>>> p1 = Foo()
>>> p1.l += [1,2,3]
>>> p1.l
[1,2,3]
>>> p2 = Foo()
>>> p2.l += [4,5,6]
>>> p1.l
[1,2,3]

Slots

By using slots you can additionally save memory and performance, while on the other hand an explicit attribute definition is required - this would prevent running into such problems as described above.

class Foo:
    __slots__ = ("i", "l")
    # l = [] would lead to a ValueError now

>>> p = Foo()    
>>> p.i
AttributeError: 'Foo' object has no attribute 'i'

Now only attributes named in __slot__ can be assigned to instances of Foo. Normally you should do this inside the constructor, but it is possible at any place:

>>> p.i = 5
>>> p.l = [1,2,3]
>>> p.i
5
>>> p.l
[1,2,3]

And of course those are instance attributes, no unintentional side effects on the class or other instances:

>>> p2 = Foo()
>>> p2.l = [5,6,7]
>>> p.l
[1,2,3]
>>> p2.l
[5,6,7]
>>> Foo.l
<member 'l' of 'Foo' objects>

Another (dis)advantage: only attributes named in __slot__ can be assigned:

>>> p.x = 10
AttributeError: 'Foo' object has no attribute 'x'

Summary

>>> import this
...
Explicit is better than implicit.
...