History.
Before Python 3.6, descriptors requiring knowledge of their attribute name relied on custom metaclasses or manual class decorators to scan the class dictionary and inject names. This approach was verbose, error-prone, and created metaclass conflicts in complex hierarchies. PEP 487 introduced the __set_name__ protocol in Python 3.6 to eliminate this boilerplate by allowing the interpreter to notify descriptors automatically.
Problem.
A descriptor instance is created during class body execution, but at that moment it has no intrinsic knowledge of the variable name it is bound to or the class it resides in. This information is essential for generating meaningful error messages, registering fields in ORM systems, or building serialization schemas. Without external notification, the descriptor remains anonymous, forcing developers to repeat the attribute name as a string argument, violating DRY principles.
Solution.
When type.__new__ constructs a class, it iterates over the namespace mapping returned by __prepare__. For each value possessing a __set_name__ method, the interpreter invokes value.__set_name__(owner_class, attribute_name). This method receives the class being constructed and the attribute string, allowing the descriptor to store this metadata. However, if a descriptor is assigned to a class attribute after the class creation process completes (monkey-patching), __set_name__ is not invoked automatically because the type machinery is no longer active.
class TrackedDescriptor: def __set_name__(self, owner, name): self.owner = owner self.name = name def __get__(self, instance, owner): if instance is None: return self return f"{self.owner.__name__}.{self.name}" class Model: field = TrackedDescriptor() # Model.field.name == 'field' # Model.field.owner == Model
Context.
While developing a configuration management library, we needed descriptors to represent environment variables. When a value was missing or invalid, the error had to specify the exact attribute name in the class (e.g., Config.database_url is required), not just a generic message.
Problem.
Initially, users had to specify the name manually: database_url = EnvVar('database_url'). This led to bugs during refactoring where the string literal and variable name diverged, causing cryptic runtime errors.
Different solutions considered:
Metaclass injection. We implemented a ConfigMeta that inspected attrs and called attr.set_name(name) on each descriptor. This worked but forced all user classes to inherit from our metaclass, breaking compatibility with other libraries using their own metaclasses like abc.ABCMeta. It also added cognitive overhead for users unfamiliar with metaclasses.
Class decorator patching. We created a @config decorator that iterated over cls.__dict__ after class creation and patched names. This avoided metaclass conflicts but was opt-in; forgetting the decorator resulted in broken descriptors. It also ran after class creation, so descriptors couldn't use their names during __init_subclass__ hooks, limiting introspection capabilities.
__set_name__ protocol. We added __set_name__ to our EnvVar descriptor. This required no changes to user code, worked automatically during class definition, and allowed the descriptor to know its name before __init_subclass__ completed, enabling early validation.
Chosen solution.
We adopted __set_name__ because it provided zero-cost abstraction for users and integrated with Python's native data model. It eliminated the metaclass collision problem entirely.
Result.
The API became declarative: database_url = EnvVar(). Refactoring tools could rename attributes safely, and error messages remained accurate. The codebase shrank by 150 lines of metaclass boilerplate, and we observed fewer bug reports related to configuration key mismatches.
When exactly is __set_name__ invoked during the class creation lifecycle?
It is invoked by type.__new__ immediately after the class body finishes executing and the namespace dictionary is populated, but before __init_subclass__ is called on parent classes. This timing is critical because it allows descriptors to finalize their state before subclasses are initialized. It does not trigger when adding attributes to an already-created class (e.g., setattr(MyClass, 'new_attr', descriptor())), because the class creation protocol has concluded. Understanding this distinction is vital for dynamic class manipulation.
Why does __set_name__ receive both the owner class and the name as arguments rather than inferring them from self?
The descriptor instance exists independently of the class; it may be instantiated before class creation and theoretically could be assigned to multiple classes (though rare). The owner argument ensures the descriptor knows the specific class where the assignment occurred, which is necessary for handling inheritance correctly. If a descriptor is defined in a base class, __set_name__ is called with the base class; if overridden in a subclass with a new instance, it is called with the subclass. This allows per-class registries without cross-contamination between base and derived classes.
How does __set_name__ interact with the __set__ and __get__ descriptor protocol methods?
__set_name__ is purely an initialization hook and does not participate in the attribute access protocol (__get__/__set__). However, it enables those methods to function correctly by providing the context needed for operations. A common mistake is assuming __set_name__ will be called again when a descriptor is inherited by a subclass that does not override it. Since the same descriptor instance is reused, __set_name__ is not re-invoked; therefore, descriptors tracking per-class state must use __init_subclass__ or check owner in __get__ to handle inheritance, rather than relying solely on __set_name__ for subclass-specific logic.