"Python Data Descriptor vs Non-Data Descriptor: Which Wins?"
"The difference between a Python data descriptor and a non-data descriptor is one line — `__set__` — and it decides whether your attribute or the instance's own dictionary wins. This comparison shows both in action, with the lookup rule, `property` under the hood, and the lazy-caching trick."
You wrote a descriptor, assigned obj.price = -5 , and your carefully crafted __set__ never fired. Or worse: your "read-only" attribute got overwritten without a whisper. If you've ever stared at a descriptor wondering why it silently does nothing, the culprit is almost always the same — the python data descriptor vs non-data descriptor distinction. It's a one-line difference in your class ( __set__ present or not), and it decides who wins every attribute access: your descriptor, or the instance's own __dict__ . Get it wrong and you burn hours debugging invisible behavior. Get it right and you unlock validation, lazy caching, and the secret of how property actually works. What's actually at stake: who wins an attribute lookup? Forget the textbook definitions for a moment. The real question is practical: when you write obj.attr , and there's both a descriptor on the class *and* a value in the instance dictionary, which one does Python return? Python resolves obj.attr in this order: Data descriptor on the class — wins, always Instance __dict__ — wins if no data descriptor claims the name Non-data descriptor on the class — only consulted if the instance dict is empty Class attributes (plain values found via the MRO) AttributeError if nothing matched That's the entire game. A *data* descriptor implements __set__ (or __delete__ ) alongside __get__ ; a *non-data* descriptor implements only __get__ . The presence of __set__ promotes your descriptor from "suggestion" to "law" — it can never be shadowed by instance data. The absence of __set__ demotes it to a polite default that any instance assignment silently overrides. This isn't academic. Django model fields, SQLAlchemy columns, property , bound methods, and functools.cached_property all sit…