Data vs Non-Data Descriptors
Swipe to show menu
Not all descriptors behave the same way in the attribute lookup chain. Python distinguishes between data descriptors and non-data descriptors based on which methods they define – and this distinction determines whether a descriptor can be shadowed by an instance attribute.
The Distinction
A data descriptor defines __set__ or __delete__ (or both) in addition to __get__. It takes priority over the instance's __dict__ in the lookup chain.
A non-data descriptor defines only __get__. It can be shadowed by an instance attribute with the same name.
123456789101112131415161718192021222324252627282930# Data descriptor – cannot be shadowed by instance __dict__ class DataDescriptor: def __get__(self, obj, objtype=None): if obj is None: return self return obj.__dict__.get("_value", "no value") def __set__(self, obj, value): obj.__dict__["_value"] = value # Non-data descriptor – can be shadowed class NonDataDescriptor: def __get__(self, obj, objtype=None): if obj is None: return self return "from descriptor" class Example: data = DataDescriptor() non_data = NonDataDescriptor() example = Example() # Trying to shadow the data descriptor example.__dict__["data"] = "shadowed attempt" print(example.data) # Still calls __get__ – data descriptor wins # Shadowing the non-data descriptor example.__dict__["non_data"] = "instance value" print(example.non_data) # "instance value" – instance __dict__ wins
Why the Distinction Matters
Data descriptors are used when you want to enforce behavior on every read and write – validation, type checking, computed properties. The instance cannot bypass the descriptor by setting an attribute directly.
Non-data descriptors are used for behavior that should be overridable per instance – functions are the primary example (covered in Chapter 4).
The Priority Table
A Practical Example
A data descriptor for validated string attributes that cannot be bypassed:
12345678910111213141516171819202122232425262728# Data descriptor enforcing non-empty strings class NonEmptyString: def __set_name__(self, owner, name): self._name = name def __get__(self, obj, objtype=None): if obj is None: return self return obj.__dict__.get(self._name, "") def __set__(self, obj, value): if not isinstance(value, str) or not value.strip(): raise ValueError(f"{self._name} must be a non-empty string, got {value!r}") obj.__dict__[self._name] = value.strip() class Employee: name = NonEmptyString() department = NonEmptyString() def __init__(self, name, department): self.name = name self.department = department employee = Employee("Alice", "Engineering") print(employee.name) # "Alice" # Trying to bypass via __dict__ has no effect – __set__ is always called employee.name = "" # Raises ValueError
Read-Only Data Descriptors
A descriptor with __get__ and __delete__ but no __set__ is also a data descriptor – and attempting to set it raises AttributeError:
123456789101112131415161718192021222324# Read-only data descriptor using __set__ to raise AttributeError class ReadOnly: def __set_name__(self, owner, name): self._name = "_" + name def __get__(self, obj, objtype=None): if obj is None: return self return obj.__dict__.get(self._name) def __set__(self, obj, value): if self._name in obj.__dict__: raise AttributeError(f"{self._name[1:]} is read-only") obj.__dict__[self._name] = value # Allowing the initial set from __init__ class Transaction: transaction_id = ReadOnly() def __init__(self, transaction_id): self.transaction_id = transaction_id # Initial set allowed transaction = Transaction("TX-001") print(transaction.transaction_id) # "TX-001" transaction.transaction_id = "TX-002" # Raises AttributeError
Thanks for your feedback!
Ask AI
Ask AI
Ask anything or try one of the suggested questions to begin our chat