Skip to Content
Course content

196: Building a Data Descriptor

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've been using the @property decorator, you've already been using descriptors without realizing it. But once you start needing the same validation logic—like ensuring a number is positive—across ten different classes, writing getters and setters for every single attribute becomes a nightmare. That's where building your own data descriptor comes in.

Wait, what actually makes it a "data" descriptor?

In Python, a descriptor is just a class that implements __get__. That's a non-data descriptor. The moment you add a __set__ method, it becomes a data descriptor. This distinction is huge because of how Python looks up attributes. If a data descriptor is defined on a class, Python will always call the descriptor's methods, even if the instance itself has an attribute with the same name in its __dict__.

I like to think of it as a "force field" around an attribute. You aren't just storing a value; you're intercepting every single attempt to read or write that value.

Where does the value actually live?

This is the part that trips people up. The descriptor instance is shared by all instances of the class it's attached to. If you store the value inside the descriptor itself (like self.value = value), every single object of that class will share the exact same value. That's a bug waiting to happen.

Instead, you use the instance argument passed into __get__ and __set__ to store the data on the object itself. Here is a concrete example: let's build a NonNegative descriptor for a product catalog.

class NonNegative:
    def __init__(self, name):
        # We store the name so we know where to put the data in the instance __dict__
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if value < 0:
            raise ValueError(f"{self.name} cannot be negative!")
        instance.__dict__[self.name] = value

class Product:
    # Now we can reuse this logic for any number of attributes
    price = NonNegative("price")
    stock = NonNegative("stock")

p = Product()
p.price = 19.99  # Works fine
p.stock = -5     # Raises ValueError: stock cannot be negative!

Isn't this just a more complicated way of writing a property?

For a single attribute? Yes. For a whole system? Absolutely not. If you have a User class, an Order class, and a Product class, and all of them have fields that must be non-negative, using @property means you're copying and pasting the same if value < 0 logic dozens of times.

By building a data descriptor, you've created a reusable piece of "attribute logic." You write the validation once, test it once, and then apply it to any field in any class across your entire project. It turns your attributes into first-class objects that carry their own rules.




📋 Practical Task

Building a ValidatedEmail Descriptor

You are building a user registration system. Instead of manually validating email addresses in every class setter, you need to create a reusable data descriptor called ValidatedEmail.

  • The ValidatedEmail descriptor should check if the value being set contains an @ symbol.
  • If the value does not contain an @, it should raise a ValueError with the message "Invalid email address".
  • The value should be stored on the instance, not the descriptor itself.
  • Create a User class that uses this descriptor for an email attribute.

Test your code by creating a User instance and attempting to set both a valid email and an invalid one.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.