Search code examples
pythonbuilt-inmutability

How to disable settattr for your class in pre __init__ state just like build-in classes do?


So if I try something like this with built-in list before init:

list.hack = 'impossible'

I get a TypeError.

TypeError: can't set attributes of built-in/extension type 'list'

But if I make my class that extends built-in list like so:

class mylist(list):
    def __setattr__(self, name, value):
        raise NotImplementedError

I can oddly do this:

mylist.hack = 'haha'

And when I would init "mylist", I would have a "hack" attribute within it.

x = mylist()
x.hack
[Out]: 'haha'

Even though I can't set any new attributes after I init "mylist", I can do so in pre init state.

Is it possible to get same pre init behaviour with custom classes, as there is with built-ins?


Solution

  • First of all, the __setattr__ def is not necessary:

    >>> class MyList(list):
    ...     pass
    
    >>> MyList.hack = 'haha'
    >>> x = MyList()
    >>> x.hack
    'haha'
    

    You are not adding an attribute to an instance (x) but to the class (MyList). (Some languages have a static keyword for these attributes (C++, Java, PHP, ...).)

    It is roughly equivalent to:

    >>> class MyList(list):
    ...     hack = 'haha' # class attribute, ie. "static"
    
    >>> x = MyList()
    >>> x.hack
    'haha'
    

    Note that this has nothing to do with pre/post init:

    >>> class MyList(list):
    ...     hack = 'haha'
    
    >>> x = MyList()
    >>> MyList.hack2 = 'hehe' # post init
    

    You have:

    >>> x.hack
    'haha'
    

    But also:

    >>> x.hack2
    'hehe'
    

    To summarize, in Python:

    • you can add class attributes after the class definition;
    • if x is an instance of C and attr an attribute of C, then x.attr is equivalent to C.attr.

    For the record, you can prevent this flexible behavior using a metaclass:

    >>> class MyListMeta(type):
    ...     def __setattr__(self, name, value):
    ...         raise AttributeError()
    
    >>> class MyList(list, metaclass=MyListMeta):
    ...     hack = 'haha'
    

    As expected:

    >>> x = MyList()
    >>> x.hack
    'haha'
    

    But now:

    >>> MyList.hack2 = 'hehe'
    Traceback (most recent call last):
    ...
    AttributeError
    

    Note that you can't set existing attributes either:

    >>> MyList.hack = 'hehe'
    Traceback (most recent call last):
    ...
    AttributeError
    

    Remarks:

    • Do not do this unless you know what you are doing.
    • Do not use this to secure your classes: one can easily bypass this behavior and add class attributes.

    Summary of the remarks: do not do this.