Understanding Python Descriptors and the __set_name__ Method
Learn how Python descriptors work with __set_name__. Control attribute access and understand descriptor protocols with clear examples.
Custom descriptors are Python objects that control how attributes are accessed, set, or deleted on other objects. The __set_name__ method is a special hook introduced in Python 3.6 that automatically tells descriptors their name and owner class when the class is created. This eliminates the need for manual name passing or complex metaclass solutions.
If you’ve ever wanted to create reusable attribute validation logic or wondered how properties work under the hood, descriptors are the answer. The __set_name__ method makes writing descriptors much cleaner and more intuitive than before.
Descriptors were first introduced in Python 2.2 through PEP 252 and PEP 253, authored by Guido van Rossum. The __set_name__ method was later added in Python 3.6 through PEP 487 by Martin Teichmann to simplify descriptor implementation.
1. Understanding Basic Descriptors
Before we tackle __set_name__, let’s understand what descriptors are and how they work.
A descriptor is any object that implements one or more of these methods:
__get__(self, instance, owner)– called when accessing the attribute__set__(self, instance, value)– called when setting the attribute__delete__(self, instance)– called when deleting the attribute
Let’s start with a simple example. We’ll create a descriptor that validates email addresses for a customer system.
Our EmailValidator class will implement the descriptor methods (__get__ and __set__) to control how email attributes are accessed and modified. Then we’ll create a Customer class with one class attribute (email) that uses our EmailValidator descriptor.
Let’s test this descriptor to see how it works:
This shows how descriptors work. When we access customer.email, Python calls the __get__ method. When we set customer.email = "something", Python calls the __set__ method. The descriptor controls what happens during these operations.
But there’s a problem here. We hardcoded the storage key '_email' in our descriptor. This means we can’t reuse this descriptor for other attributes.
2. The Problem Before __set_name__
Let’s see what happens when we try to create multiple validators. Say we want to validate both email and phone numbers with similar logic.
We’ll need to create separate EmailValidator and PhoneValidator classes, each with their own descriptor methods (__get__ and __set__). Then we’ll create a Customer class with two class attributes (email and phone) using these different validators.
We’re duplicating code! Each validator needs to know its own storage key. This is inefficient and error-prone.
The old solution was to manually pass the name to the descriptor during initialization.
We’d create a generic FieldValidator class that accepts the field name, plus the descriptor methods that use the provided field name. Then we’d create a Customer class with two class attributes (email and phone) where we manually pass the field names as strings.
This approach has problems. You have to remember to keep the attribute name and the string parameter in sync. If you change the attribute name, you must remember to change the string too. This violates the DRY (Don’t Repeat Yourself) principle.
3. Enter __set_name__
The __set_name__ method solves this problem elegantly. Python automatically calls this method when a class is created, passing the owner class and the attribute name.
Let’s look at the signature.
def __set_name__(self, owner, name):
# owner: the class that owns this descriptor
# name: the name of the attribute this descriptor is assigned to
Now let’s rewrite our validator using __set_name__.
Our improved FieldValidator class will have a __set_name__ method that automatically receives the field name from Python, plus the descriptor methods that use the automatically-provided name.
Let’s test this improved version:
Much cleaner! The descriptor automatically knows its name and can create the appropriate private attribute name. Python calls __set_name__ during class creation, so our descriptor receives the name 'email' for the first instance and 'phone' for the second instance.
4. How __set_name__ Works Step by Step
Let’s understand exactly when and how __set_name__ gets called. We’ll create a debugging descriptor that shows us what’s happening.
Our DebugDescriptor class will implement all three descriptor methods (__set_name__, __get__, and __set__) with print statements to show when each is called. Then we’ll create a TestClass with two class attributes (attr1 and attr2) that are instances of our debugging descriptor.
When you run this code, you’ll see that __set_name__ is called during class creation, before any instances are created. This is exactly what we want – the descriptor learns its name as soon as the class is defined.
5. Real-World Example: Car Configuration System
Now let’s build something more practical. We’ll create a car configuration system that validates different types of attributes.
Our ConfigField class will be a descriptor that accepts validation functions and default values, plus the standard descriptor methods. Then we’ll create validation functions and a Car class with class attributes that use our ConfigField descriptor.
Let’s test our car configuration system:
This demonstrates how __set_name__ makes our descriptors completely reusable. Each ConfigField instance automatically knows its own name and can provide meaningful error messages.
6. Understanding the Timing
Let’s understand exactly when __set_name__ is called in the class creation process.
We’ll create a TimingDescriptor class that has both an __init__ method and a __set_name__ method, each with print statements. Then we’ll define a TestClass with two class attributes (attr1 and attr2) that are instances of our timing descriptor, with print statements between the assignments to show the execution order.
Starting class definition...
Inside class body - before descriptor assignment
TimingDescriptor.__init__ called with value: first
Inside class body - after first descriptor assignment
TimingDescriptor.__init__ called with value: second
Inside class body - after second descriptor assignment
TimingDescriptor.__set_name__ called: owner=TestClass, name=attr1
TimingDescriptor.__set_name__ called: owner=TestClass, name=attr2
Class definition complete
When you run this code, you’ll see that __init__ is called immediately when each descriptor instance is created (during the class attribute assignments), but __set_name__ is called after the entire class body has been executed. This timing is important – it means the descriptor has access to the complete class object when __set_name__ is called, not just the partial class that’s still being constructed.
7. Best Practices
Here are some important best practices when using __set_name__:
Always Check for None in get
This check is crucial. When you access the descriptor through the class (like MyClass.attr), instance will be None. We return the descriptor itself in this case.
Use Unique Private Names
This prevents naming conflicts when the same descriptor is used in multiple classes.
Handle Edge Cases Gracefully
This ensures your descriptor behaves correctly in all situations.