2026, Jan 07 17:00
Python __slots__ and inheritance: preventing new attributes when a slotless base provides __dict__
Learn why Python __slots__ don't block new attributes when inheriting a slotless base with __dict__, and how to enforce restrictions with a metaclass.
In Python, a common use of __slots__ is to prevent instances from getting arbitrary new attributes. It feels straightforward until inheritance gets involved: a subclass that inherits a __dict__ from a slotless base will accept new attributes even if the subclass declares __slots__. Understanding why this happens and how to set guardrails around it helps avoid surprising behavior in real code.
Problem
Defining __slots__ on a standalone class blocks creation of attributes not listed in the slots. That breaks down when the class inherits from a slotless base that provides a __dict__.
Minimal reproducible example
First, the restrictive case works as expected:
class Gadget:
__slots__ = ("a", "b")
inst = Gadget()
inst.c = "hi" # error
Now compare that to a subclass of a slotless base:
class BaseLoose:
pass
class Gadget(BaseLoose):
__slots__ = ("a", "b")
inst = Gadget()
inst.c = "hi" # ok
Why it happens
The subclass inherits the __dict__ from the slotless parent, and that __dict__ enables attaching additional attributes. Even though the subclass declares __slots__, the inherited __dict__ still exists, so the attribute assignment succeeds.
This also touches a design nuance frequently framed via the Liskov Substitution Principle: subclasses aren’t supposed to prohibit operations their superclasses allow. In languages with access modifiers, you can’t make an overridden method more restrictive for the same reason—doing so would break valid superclass calls. While Python has no access modifiers and is permissive about attribute creation, the same general idea explains why restricting attribute capabilities in a subclass of a slotless parent conflicts with the parent’s behavior.
Practical workaround with a metaclass
If you control the class hierarchy, you can enforce empty __slots__ by default for subclasses via a metaclass that pre-populates the class namespace with an empty tuple for "__slots__" using __prepare__. Subclasses may still explicitly define their own __slots__, which is often desirable.
class NoNewAttrsMeta(type):
@classmethod
def __prepare__(mcls, cls_name, parent_types):
return {"__slots__": ()}
class Alpha(metaclass=NoNewAttrsMeta):
__slots__ = ("x", "y")
def __init__(self, x=0, y=0):
self.x = x
self.y = y
class Beta(Alpha):
pass
class Gamma(Alpha):
__slots__ = ("z",)
def __init__(self, x=0, y=0, z=0):
super().__init__(x, y)
self.z = z
With this setup, attempts to attach attributes not present in slots on instances of Alpha or Beta raise AttributeError, while Gamma can use its declared z slot. This puts reasonable guardrails in place without preventing subclasses from declaring their own slots.
It’s also worth noting the simple, explicit approach in the same spirit: starting a class with __slots__ = tuple() blocks a __dict__ for that class.
A sharp edge you must account for
If a class with such a metaclass inherits from a slotless base, adding new attributes still works. The inherited __dict__ from the base wins, and it has to work that way because a slotless superclass generally needs a __dict__ for its own attributes unless it defined slots itself.
class PlainBase:
pass
class Delta(PlainBase, metaclass=NoNewAttrsMeta):
__slots__ = ("x", "y")
Delta().z = 1 # still works
This limitation follows directly from the parent class design. If you can modify the parent, giving it __slots__ changes the picture. Otherwise, you can’t retroactively take away the __dict__ it provides.
Why this matters
Relying on __slots__ for attribute restriction across an inheritance boundary can lead to incorrect assumptions when the base class is slotless. Knowing that the __dict__ is inherited explains the behavior and helps you set expectations. You can introduce sensible constraints with a metaclass or by explicitly declaring empty slots, but you shouldn’t expect bulletproof enforcement when the parent class was designed to allow new attributes.
Separately, remember that __slots__ weren’t introduced primarily to restrict attributes. They mainly exist as a memory optimization because a regular instance carries a dict. Using slots to limit attributes is a common pattern, but it’s secondary to the feature’s original purpose.
Takeaways
If you need to prevent new attributes, do it where you control the hierarchy. A metaclass that injects empty __slots__ gives you a sane default for subclasses, and explicit __slots__ in those subclasses remain possible. If a slotless base provides a __dict__, you can’t block it from a subclass. If you can change the parent, giving it __slots__ aligns its behavior with your constraint. Otherwise, design with the inherited __dict__ in mind and avoid relying on subclass-only restrictions that contradict the parent’s capabilities.