pythoninheritancesubclass

Is it possible to limit attributes in a Python sub class using __slots__?


One use of __slots__ in Python is to disallow new attributes:

class Thing:
    __slots__ = 'a', 'b'

thing = Thing()
thing.c = 'hello'   #   error

However, this doesn’t work if a class inherits from another slotless class:

class Whatever:
    pass

class Thing(Whatever):
    __slots__ = 'a', 'b'

thing = Thing()
thing.c = 'hello'   #   ok

That’s because it also inherits the __dict__ from its parent which allows additional attributes.

Is there any way of blocking the __dict__ from being inherited?

It seems to me that this would allow a sub class to be less generic that its parent, so it’s surprising that it doesn’t work this way naturally.

Comment

OK, the question arises as whether this would violate the https://en.wikipedia.org/wiki/Liskov_substitution_principle . This, in turn buys into a bigger discussion on inheritance.

Most books would, for example, suggest that a circle is an ellipse so a Circle class should inherit from an Ellipse class. However, since a circle is more restrictive, this would violate the Liskov Substitution Principle in that a sub class should not do less than the parent class.

In this case, I’m not sure about whether it applies here. Python has no access modifiers, so object data is already over-exposed. Further, without __slots__ Python objects are pretty promiscuous about adding additional attributes, and I’m not sure that’s really part of the intended discussion.


Solution

  • If you are willing to use a metaclass, you can prevent this. Simply insert an empty sequence for '__slots__' in the namespace returned by __prepare__ this is a hook that prepares the namespace that will be used for the class, it defaults to a normal dict(), and we can just force the subclass to have an empty (not unspecified) __slots__

    class EmptySlotsMeta(type):
        @classmethod
        def __prepare__(metacls, name, bases):
            return {"__slots__":()}
    
    class Foo(metaclass=EmptySlotsMeta):
        __slots__ = 'x', 'y'
        def __init__(self, x=0, y=0):
            self.x = x
            self.y = y
    
    class Bar(Foo):
        pass
    
    class Baz(Foo):
        __slots__ = ('z',)
        def __init__(self, x=0, y=0, z=0):
            super().__init__(x, y)
            self.z = z
    

    Now, in a REPL:

    >>> foo = Foo()
    >>> bar = Bar()
    >>> baz = Baz()
    >>> foo.z = 99
    Traceback (most recent call last):
      File "<python-input-25>", line 1, in <module>
        foo.z = 99
        ^^^^^
    AttributeError: 'Foo' object has no attribute 'z' and no __dict__ for setting new attributes
    >>> bar.z = 99
    Traceback (most recent call last):
      File "<python-input-26>", line 1, in <module>
        bar.z = 99
        ^^^^^
    AttributeError: 'Bar' object has no attribute 'z' and no __dict__ for setting new attributes
    >>> baz.z = 99
    

    Note, a class with this metaclass can still define their own __slots__ (as in Foo or its second subclass Baz above). That's probably desirable. As with most things in Python, you can put some guardrails around it but it isn't worth trying to make it bulletproof.

    Note, although it is commonly used this way, __slots__ were not added for this use-case, that is, to restrict attributes. It exists mainly as a memory optimization, since a standard instance carries around a whole dict object.

    Edit:

    I undeleted this on the OP's request, but I'm still not sure it answers the exact question, note:

    class Whatever:
        pass
    
    class Foo(Whatever, metaclass=EmptySlotsMeta):
        __slots__ = ('x','y')
    
    Foo().z = 1 # totally works
    

    But it has to work this way, because the superclass will almost certainly create attributes, and it needs a __dict__ to do that unless it had defined slots.