C.W.K.
Stream
Lesson 05 of 05 · published

Descriptors and __slots__ — The Lower-Level Tools

~22 min · descriptor, slots, memory, advanced

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

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.

Principle: Descriptors and __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.

Code

A simple descriptor — validation·python
class Positive:
    def __set_name__(self, owner, name):
        self.attr = f"_{name}"

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return getattr(instance, self.attr)

    def __set__(self, instance, value):
        if value <= 0:
            raise ValueError("must be positive")
        setattr(instance, self.attr, value)

class Account:
    balance = Positive()           # descriptor

    def __init__(self, initial):
        self.balance = initial      # uses descriptor's __set__

a = Account(100)
print(a.balance)                # 100  — uses __get__
a.balance = 200                 # uses __set__
print(a.balance)                # 200

try:
    a.balance = -50             # __set__ validates
except ValueError as e:
    print(e)
How @property is built — descriptors in stdlib·python
# Equivalent to @property — Python's actual implementation uses descriptors
class MyProperty:
    def __init__(self, fget):
        self.fget = fget

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.fget(instance)

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @MyProperty
    def area(self):
        return 3.14159 * self.radius ** 2

print(Circle(5).area)        # 78.54  — works just like @property
__slots__ — memory and attribute lockdown·python
class Point:
    __slots__ = ("x", "y")

    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(3, 4)
print(p.x, p.y)             # 3 4

# But you can't add new attributes
try:
    p.z = 5
except AttributeError as e:
    print(e)                # 'Point' object has no attribute 'z'

# Memory savings are significant for many instances
import sys

class Regular:
    def __init__(self, x, y):
        self.x, self.y = x, y

class Slotted:
    __slots__ = ("x", "y")
    def __init__(self, x, y):
        self.x, self.y = x, y

print(sys.getsizeof(Regular(1, 2).__dict__) + sys.getsizeof(Regular(1, 2)))
# Slotted instance has no __dict__ — significantly smaller
@dataclass(slots=True) — modern way·python
from dataclasses import dataclass

@dataclass(slots=True)            # 3.10+
class Point:
    x: int
    y: int

p = Point(3, 4)
print(p)

try:
    p.z = 5
except AttributeError as e:
    print(e)

# All the dataclass benefits + the memory and lockdown of __slots__

External links

Exercise

Implement a TypedAttribute descriptor that enforces a type at assignment. class Thing: count = TypedAttribute(int); name = TypedAttribute(str). Setting t.count = "five" should raise TypeError. Use __set_name__ so the descriptor knows its name and stores per-instance state under a private attribute name (_count, _name). Test all paths. Then add __slots__ to the class and observe what changes — note that __slots__ + descriptors interact in subtle ways.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.