Complete guide to Python's __set__ method covering descriptors, attribute management, and property customization.
Last modified April 8, 2025
This comprehensive guide explores Python’s set method, the special method used in descriptors to customize attribute assignment. We’ll cover basic usage, property-like descriptors, validation, and practical examples.
The set method is part of Python’s descriptor protocol. It’s called when an attribute is assigned a value on an instance. Descriptors allow customizing attribute access.
Key characteristics: it takes three parameters (self, instance, value), doesn’t return anything, and is invoked during assignment. It’s used with get and optionally delete for full descriptor implementation.
Here’s a simple descriptor implementation showing how set intercepts attribute assignments. This demonstrates the basic descriptor pattern.
basic_set.py
class LoggedAttribute: def set(self, instance, value): print(f"Setting value {value} on {instance}") instance.dict[self.name] = value
def __set_name__(self, owner, name):
self.name = name
class Person: name = LoggedAttribute() age = LoggedAttribute()
p = Person() p.name = “Alice” # Prints “Setting value Alice on <main.Person object…>” p.age = 30 # Prints “Setting value 30 on <main.Person object…>”
This LoggedAttribute descriptor logs all assignments to attributes that use it. The set_name method captures the attribute name for storage in the instance’s dict.
The set method receives the descriptor instance, the owner instance, and the value being assigned. It stores the value in the instance’s namespace.
set is perfect for implementing validated attributes that enforce constraints on assigned values. Here’s an age validator example.
validation.py
class ValidatedAge: def set(self, instance, value): if not isinstance(value, int): raise TypeError(“Age must be an integer”) if not 0 <= value <= 120: raise ValueError(“Age must be between 0 and 120”) instance.dict[‘age’] = value
class Person: age = ValidatedAge()
p = Person() p.age = 25 # Works
This descriptor validates that age assignments are integers within a reasonable range. Invalid assignments raise exceptions with helpful messages.
The validation happens transparently during attribute assignment. The descriptor pattern keeps validation logic separate from the class while maintaining clean attribute access syntax.
Descriptors with set can create property-like attributes with custom get/set behavior. Here’s a temperature converter example.
property_like.py
class Celsius: def get(self, instance, owner): return instance._celsius
def __set__(self, instance, value):
instance._celsius = value
instance._fahrenheit = value * 9/5 + 32
class Temperature: celsius = Celsius()
def __init__(self, celsius=0):
self.celsius = celsius
@property
def fahrenheit(self):
return self._fahrenheit
temp = Temperature(100) print(temp.fahrenheit) # 212.0 temp.celsius = 0 print(temp.fahrenheit) # 32.0
This descriptor automatically updates the Fahrenheit equivalent whenever Celsius is set. The set method handles the conversion and storage.
The Temperature class exposes both temperature scales while only storing Celsius internally. The descriptor maintains consistency between the two representations.
A descriptor can make attributes read-only by implementing set to prevent modifications. This example shows a constant-like attribute.
readonly.py
class ReadOnly: def init(self, value): self.value = value
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
raise AttributeError("Cannot modify read-only attribute")
class Configuration: VERSION = ReadOnly(“1.0.0”)
config = Configuration() print(config.VERSION) # “1.0.0”
This descriptor stores the value during initialization and allows reading but not writing. Attempts to modify the attribute raise an AttributeError.
The set method completely blocks assignment attempts. This pattern is useful for constants or configuration values that shouldn’t change after initialization.
Descriptors can implement lazy initialization, deferring computation until first access. Here’s a lazy-loaded attribute example.
lazy.py
class LazyProperty: def init(self, factory): self.factory = factory self.name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
value = self.factory(instance)
instance.__dict__[self.name] = value
return value
def __set__(self, instance, value):
instance.__dict__[self.name] = value
class DataProcessor: def init(self, data): self.data = data
@LazyProperty
def processed_data(self):
print("Processing data...")
return [x * 2 for x in self.data]
processor = DataProcessor([1, 2, 3]) print(processor.processed_data) # Processes and prints [2, 4, 6] print(processor.processed_data) # Uses cached value
This descriptor computes the value only on first access, then caches it. The set allows explicit setting to bypass the lazy computation.
The factory function is called only when the attribute is first accessed. This is useful for expensive computations that might not always be needed.
Store in instance dict: Avoid infinite recursion by not using direct attribute access
Implement set_name: For Python 3.6+ to automatically get attribute names
Consider thread safety: Add locks if descriptors are used in multi-threaded code
Document behavior: Clearly document any special assignment logic
Use properties for simple cases: Prefer @property for single-class use
My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.
List all Python tutorials.