When Descriptors Get Smart with __set_name__
Learn how Python's __set_name__ method lets descriptors automatically know their attribute names, removing manual string passing and reducing bugs in reusable validation and logging tools.
Here’s the article you requested, written for PythonSkillset.com.
When Descriptors Get Smart with __set_name__
You’ve probably heard of Python descriptors — they’re the secret sauce behind @property, classmethod, and staticmethod. But there’s one humble method that makes descriptors extra smart: __set_name__.
It saves you from passing attribute names manually. And if you’ve ever built reusable validation, logging, or type-checking tools, this little feature can clean up your code a lot.
The problem it solves
Imagine you have a descriptor for ensuring an attribute is always a positive integer:
class PositiveInt:
def __get__(self, obj, objtype=None):
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not isinstance(value, int) or value <= 0:
raise ValueError("Must be positive int")
obj.__dict__[self.name] = value
The issue? The descriptor doesn’t know its own attribute name inside the class. So you end up doing something like:
class Person:
age = PositiveInt("age") # yuck, repeated name
That’s fragile and annoying. If you rename the attribute, you have to remember to change the string too.
Enter __set_name__
When Python creates the class, it calls __set_name__ on each descriptor, passing it the class and the attribute name. So you can write:
class PositiveInt:
def __set_name__(self, owner, name):
self.public_name = name
self.private_name = "_" + name
def __get__(self, obj, objtype=None):
return getattr(obj, self.private_name)
def __set__(self, obj, value):
if not isinstance(value, int) or value <= 0:
raise ValueError("Must be positive int")
setattr(obj, self.private_name, value)
Now this works without any manual naming:
class Person:
age = PositiveInt()
score = PositiveInt()
p = Person()
p.age = 30 # works
p.score = -5 # raises ValueError
No more "age" strings. The descriptor figures it out automatically.
A real-world flavor
At PythonSkillset, we saw a team using this pattern for a config validator. Every config field had type constraints, min/max, and default values. Without __set_name__, they had a huge dictionary mapping field names to validators. With it, each validator knew its own field just by being assigned inside the class.
Here’s a simplified version:
class ConfigField:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
return obj.__dict__.get(self.name, self.default)
def __set__(self, obj, value):
if not self.validate(value):
raise ValueError(f"Invalid value for {self.name}")
obj.__dict__[self.name] = value
def validate(self, value):
raise NotImplementedError
Then you subclass for specifics like IntField, StringField, etc. No manual wiring needed.
How it fits the bigger picture
__set_name__ works for all descriptors, not just data descriptors. You can use it in a logging decorator too:
class LoggedAttribute:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
print(f"Accessing {self.name}")
return obj.__dict__.get(self.name)
Every time someone reads the attribute, it logs the name automatically.
A quick gotcha
__set_name__ is called once when the class is created. If you dynamically add a descriptor to a class later (like MyClass.attr = SomeDescriptor()), __set_name__ won’t fire. For that, you’d need to call it manually. But in practice, that’s rare.
The takeaway
If you ever write a descriptor, use __set_name__ to capture its own attribute name. It removes redundancy and prevents bugs when renaming. That’s a small detail that makes a big difference in maintainable code.
Python keeps giving us these elegant helpers. This one is definitely worth keeping in your back pocket.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.