Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
196: Building a Data Descriptor
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
ValidatedEmaildescriptor should check if the value being set contains an@symbol. - If the value does not contain an
@, it should raise aValueErrorwith the message"Invalid email address". - The value should be stored on the instance, not the descriptor itself.
- Create a
Userclass that uses this descriptor for anemailattribute.
Test your code by creating a User instance and attempting to set both a valid email and an invalid one.
There are no comments for now.