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.
class EmailValidator:
def __get__(self, instance, owner):
if instance is None:
return self
# Get the value from the instance's __dict__
return instance.__dict__.get('_email', None)
def __set__(self, instance, value):
if not isinstance(value, str):
raise ValueError("Email must be a string")
if '@' not in value:
raise ValueError("Email must contain @ symbol")
# Store the value in the instance's __dict__
instance.__dict__['_email'] = value
class Customer:
email = EmailValidator()
def __init__(self, name, email):
self.name = name
self.email = email
Let’s test this descriptor to see how it works:
# Test it
customer = Customer("John", "[email protected]")
print(customer.email) # [email protected]
try:
customer.email = "invalid-email"
except ValueError as e:
print(f"Error: {e}") # Error: Email must contain @ symbol
[email protected]
Error: Email must contain @ symbol
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.
class EmailValidator:
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get('_email', None)
def __set__(self, instance, value):
if not isinstance(value, str):
raise ValueError("Email must be a string")
if '@' not in value:
raise ValueError("Email must contain @ symbol")
instance.__dict__['_email'] = value
class PhoneValidator:
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get('_phone', None)
def __set__(self, instance, value):
if not isinstance(value, str):
raise ValueError("Phone must be a string")
if not value.startswith('+'):
raise ValueError("Phone must start with +")
instance.__dict__['_phone'] = value
class Customer:
email = EmailValidator()
phone = PhoneValidator()
def __init__(self, name, email, phone):
self.name = name
self.email = email
self.phone = phone
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.
class FieldValidator:
def __init__(self, field_name):
self.field_name = field_name
self.private_name = f'_{field_name}'
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.private_name, None)
def __set__(self, instance, value):
if not isinstance(value, str):
raise ValueError(f"{self.field_name} must be a string")
instance.__dict__[self.private_name] = value
class Customer:
email = FieldValidator('email') # Manual name passing
phone = FieldValidator('phone') # Manual name passing
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.
class FieldValidator:
def __set_name__(self, owner, name):
self.field_name = name
self.private_name = f'_{name}'
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.private_name, None)
def __set__(self, instance, value):
if not isinstance(value, str):
raise ValueError(f"{self.field_name} must be a string")
instance.__dict__[self.private_name] = value
class Customer:
email = FieldValidator() # No manual name needed!
phone = FieldValidator() # No manual name needed!
def __init__(self, name, email, phone):
self.name = name
self.email = email
self.phone = phone
Let’s test this improved version:
customer = Customer("John", "[email protected]", "+1234567890")
print(f"Email: {customer.email}") # Email: [email protected]
print(f"Phone: {customer.phone}") # Phone: +1234567890
Email: [email protected]
Phone: +1234567890
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.
class DebugDescriptor:
def __set_name__(self, owner, name):
print(f"__set_name__ called: owner={owner.__name__}, name={name}")
self.name = name
self.private_name = f'_{name}'
def __get__(self, instance, owner):
print(f"__get__ called for {self.name}")
if instance is None:
return self
return instance.__dict__.get(self.private_name, f"default_{self.name}")
def __set__(self, instance, value):
print(f"__set__ called for {self.name} with value: {value}")
instance.__dict__[self.private_name] = value
print("Creating TestClass...")
class TestClass:
attr1 = DebugDescriptor()
attr2 = DebugDescriptor()
def __init__(self):
print("TestClass instance created")
print("\nCreating instance...")
obj = TestClass()
print("\nAccessing attributes...")
print(obj.attr1)
print(obj.attr2)
Creating TestClass...
__set_name__ called: owner=TestClass, name=attr1
__set_name__ called: owner=TestClass, name=attr2
Creating instance...
TestClass instance created
Accessing attributes...
__get__ called for attr1
default_attr1
__get__ called for attr2
default_attr2
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.
class ConfigField:
def __init__(self, validator=None, default=None):
self.validator = validator
self.default = default
def __set_name__(self, owner, name):
self.name = name
self.private_name = f'_{name}'
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.private_name, self.default)
def __set__(self, instance, value):
if self.validator:
if not self.validator(value):
raise ValueError(f"Invalid value for {self.name}: {value}")
instance.__dict__[self.private_name] = value
# Let's define some validation functions
def validate_engine_type(value):
return value in ['V6', 'V8', 'Electric', 'Hybrid']
def validate_year(value):
return isinstance(value, int) and 2000 <= value <= 2025
def validate_price(value):
return isinstance(value, (int, float)) and value > 0
class Car:
engine_type = ConfigField(validate_engine_type, default='V6')
year = ConfigField(validate_year, default=2023)
price = ConfigField(validate_price, default=25000)
def __init__(self, model, **kwargs):
self.model = model
for key, value in kwargs.items():
if hasattr(self, key):
setattr(self, key, value)
def __str__(self):
return f"{self.year} {self.model} ({self.engine_type}) - ${self.price:,}"
Let’s test our car configuration system:
# Test the car configuration
car1 = Car("Sedan", engine_type="V8", year=2024, price=35000)
print(car1) # 2024 Sedan (V8) - $35,000
car2 = Car("SUV") # Uses defaults
print(car2) # 2023 SUV (V6) - $25,000
# Test validation
try:
car3 = Car("Truck", engine_type="V12") # Invalid engine type
except ValueError as e:
print(f"Error: {e}") # Error: Invalid value for engine_type: V12
try:
car4 = Car("Coupe", year=1990) # Invalid year
except ValueError as e:
print(f"Error: {e}") # Error: Invalid value for year: 1990
2024 Sedan (V8) - $35,000
2023 SUV (V6) - $25,000
Error: Invalid value for engine_type: V12
Error: Invalid value for year: 1990
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.
class TimingDescriptor:
def __init__(self, value):
print(f"TimingDescriptor.__init__ called with value: {value}")
self.value = value
def __set_name__(self, owner, name):
print(f"TimingDescriptor.__set_name__ called: owner={owner.__name__}, name={name}")
self.name = name
print("Starting class definition...")
class TestClass:
print("Inside class body - before descriptor assignment")
attr1 = TimingDescriptor("first")
print("Inside class body - after first descriptor assignment")
attr2 = TimingDescriptor("second")
print("Inside class body - after second descriptor assignment")
print("Class definition complete")
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
class MyDescriptor:
def __set_name__(self, owner, name):
self.name = name
self.private_name = f'_{name}'
def __get__(self, instance, owner):
if instance is None:
return self # Return descriptor when accessed via class
return instance.__dict__.get(self.private_name)
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
class MyDescriptor:
def __set_name__(self, owner, name):
self.name = name
self.private_name = f'_{owner.__name__}_{name}' # More unique
This prevents naming conflicts when the same descriptor is used in multiple classes.
Handle Edge Cases Gracefully
class RobustDescriptor:
def __set_name__(self, owner, name):
self.name = name
self.private_name = f'_{name}'
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.private_name)
def __set__(self, instance, value):
if instance is None:
raise AttributeError("Cannot set attribute on class")
instance.__dict__[self.private_name] = value
This ensures your descriptor behaves correctly in all situations.



