Descriptors — the protocol behind property, classmethod, and more
A descriptor is any object that defines __get__, __set__, or __delete__. When such an object is stored as a class attribute, accessing it on an instance goes through those methods instead of returning the object directly. @property is implemented as a descriptor. So is @classmethod, @staticmethod, and most ORM field types.
Data vs non-data descriptors
A descriptor with __set__ is a data descriptor; without it, it's a non-data descriptor. The difference matters for lookup: data descriptors win over instance attributes, non-data descriptors lose to them. property is a data descriptor. Functions (which become methods) are non-data descriptors — that's why you can shadow a method by assigning to self.method = something.
__set_name__ — knowing your own name
Python 3.6+ added __set_name__(self, owner, name), called when a descriptor is assigned to a class attribute. It tells the descriptor what name it was given. Useful for descriptors that need to manage per-instance state — store it under a related name on the instance, like _x for descriptor x.
__slots__ — memory and attribute control
By default, every Python instance has a __dict__ that lets you add any attribute. __slots__ = ("x", "y") on a class tells Python: this class's instances have ONLY these attributes, no __dict__. Memory savings are significant (no per-instance dict). Trying to set any other attribute raises AttributeError.
When __slots__ pays off
When you create millions of instances. For ten or a hundred objects, the __dict__ overhead is noise. For a million, the savings are real. Use @dataclass(slots=True) (3.10+) for the dataclass version. Note: __slots__ classes can't have arbitrary attributes added later, which is exactly the point but also the price.
__slots__ are tools you reach for when you need them — not by default. Most application code doesn't touch them. Library and framework code uses them more often. Recognize them when you see them; reach for them when the use case is real.