← Home ← Codex ← DEBT ← Engine
Browse by Category
+ added · updated 7d
← Back to glossary

Python Descriptor Protocol

Python Python 3.6+ Advanced
debt(d8/e5/b5/t7)
d8 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'silent in production until users hit it' (d9), pulled to d8. detection_hints.automated is 'no' and the only signal is a regex code_pattern for self-assignment in __set__/__delete__; storing per-instance state on the shared descriptor produces silent cross-instance corruption that surfaces only at runtime with multiple instances, not caught by standard linters/type checkers.

e5 Effort Remediation debt — work required to fix once spotted

Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix (move per-instance state into instance __dict__ via __set_name__ or a WeakKeyDictionary) is more than a one-line swap — it changes the descriptor's storage model and every read/write path within the descriptor class, though it stays localised to that component.

b5 Burden Structural debt — long-term weight of choosing wrong

Closest to 'persistent productivity tax' (b5). applies_to library/web/cli and tags metaprogramming/data-model: descriptors are load-bearing for ORM fields, typed-config, and reusable validation across many attributes/classes, so a flawed descriptor reaches many work streams that depend on the field abstraction.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t7). The misconception (descriptors treated as a fancy instance-wide __getattr__) plus data vs non-data precedence and shared-state-on-descriptor mistakes contradict how attribute access appears to work elsewhere; the 'obvious' instance-level mental model is wrong and produces silent shared state.

About DEBT scoring →

Also Known As

descriptors __get__ __set__ data descriptor non-data descriptor

TL;DR

Objects implementing __get__/__set__/__delete__ that intercept attribute access at the class level — the machinery behind property, classmethod, and ORM fields.

Explanation

A descriptor is any object defining at least one of __get__(self, obj, objtype), __set__(self, obj, value), or __delete__(self, obj). When such an object is assigned as a class attribute, Python routes attribute access through these methods instead of returning the descriptor itself. This is fundamentally different from __getattr__/__getattribute__, which live on the instance's class and act as fallbacks for the whole object: descriptors are per-attribute and resolved during the normal attribute lookup chain. There are two kinds. A data descriptor defines __set__ or __delete__ (plus usually __get__) and takes priority over the instance __dict__ — this is why property assignments are intercepted even when an instance has a same-named entry. A non-data descriptor defines only __get__ and is shadowed by instance __dict__ entries, which is how functions become bound methods yet can be overridden per instance. The lookup order for instance.attr is: data descriptors on the type, then instance __dict__, then non-data descriptors and class attributes. Built-ins property, classmethod, staticmethod, and functions are all descriptors. Since Python 3.6, __set_name__(self, owner, name) is called automatically when the owning class is created, letting a descriptor learn the attribute name it was bound to without repeating it. Descriptors shine when you need reusable, validated, computed, or lazily-loaded attributes across many fields — ORMs (Django, SQLAlchemy), typed-field libraries, and caching decorators rely on them. The common error is storing per-instance state on the descriptor itself; because one descriptor instance is shared by every instance of the owning class, state must live in the instance __dict__ or a WeakKeyDictionary keyed by instance, not on self. For single computed attributes, property is the simpler, idiomatic choice; reach for a custom descriptor only when the same access logic must be reused across multiple attributes or classes.

Common Misconception

Descriptors are just a fancy way to write __getattr__ on an instance. In reality descriptors are class-level, per-attribute hooks resolved during normal lookup, while __getattr__/__getattribute__ are instance-wide fallbacks that fire for any missing or every attribute respectively.

Why It Matters

Descriptors power property, methods, and ORM fields; misunderstanding data vs non-data precedence or storing per-instance state on the shared descriptor produces silent cross-instance data corruption.

Common Mistakes

  • Storing per-instance values on the descriptor object (self.value), so every instance shares and overwrites the same state.
  • Confusing data and non-data descriptors and being surprised that instance __dict__ shadows a __get__-only descriptor.
  • Hardcoding the attribute name instead of using __set_name__ to learn it automatically in Python 3.6+.
  • Reaching for a custom descriptor when a simple property would express a single computed attribute more clearly.
  • Forgetting to handle obj is None in __get__, which breaks class-level access like MyClass.field.

Avoid When

  • A single computed or validated attribute is needed — a property is simpler and more readable.
  • The logic is not reused across multiple attributes or classes, so the indirection adds no value.
  • Team members are unfamiliar with the data model and the implicit lookup order would obscure behavior.

When To Use

  • The same access, validation, or caching logic must be reused across many attributes or classes.
  • Building field abstractions for ORMs, typed-config, or lazy-loaded computed properties.
  • You need data-descriptor precedence to guarantee interception even when instance __dict__ holds a same-named key.

Code Examples

✗ Vulnerable
class Positive:
    def __get__(self, obj, objtype=None):
        return self.value          # shared across ALL instances!
    def __set__(self, obj, value):
        if value < 0:
            raise ValueError('must be positive')
        self.value = value         # bug: stored on descriptor, not obj

class Account:
    balance = Positive()

a = Account(); a.balance = 100
b = Account(); b.balance = 5
print(a.balance)  # 5 — a and b share descriptor state
✓ Fixed
class Positive:
    def __set_name__(self, owner, name):
        self.name = name           # learn the attribute name

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self            # class-level access
        return obj.__dict__[self.name]

    def __set__(self, obj, value):
        if value < 0:
            raise ValueError('must be positive')
        obj.__dict__[self.name] = value  # per-instance storage

class Account:
    balance = Positive()

a = Account(); a.balance = 100
b = Account(); b.balance = 5
print(a.balance)  # 100 — state isolated per instance

Added 3 Jun 2026
Views 103
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings W 0 pings T 0 pings F 1 ping S 0 pings S 1 ping M 0 pings T 0 pings W 0 pings T 0 pings F 2 pings S 0 pings S 0 pings M 2 pings T 0 pings W 0 pings T 0 pings F 1 ping S 0 pings S 0 pings M 1 ping T 0 pings W 1 ping T 0 pings F 1 ping S 1 ping S 1 ping M 0 pings T 0 pings W 0 pings T
No pings yet today
No pings yesterday
PetalBot 8 SEMrush 7 Amazonbot 7 ChatGPT 7 Ahrefs 6 Google 4 Brave Search 4 Bing 3 Meta AI 2 Applebot 2 Perplexity 1 Claude 1 Scrapy 1 Common Crawl 1 Twitter/X 1 Unknown AI 1
crawler 54 crawler_json 2
🧱 FUNDAMENTALS — new to this? Start with the ground floor.
Python general Python is a programming language known for readable syntax and versatility, used for web development, data science, automation, and more.

Python's gentle learning curve makes it an ideal first language, while its vast ecosystem keeps it relevant for machine learning, APIs, and DevOps. Skills transfer directly to professional environments because Python runs in production at companies of every size.

💡 When Python throws IndentationError, check that every block uses the same whitespace style—pick spaces (preferably 4) and stick with them everywhere.

Ask Codex about Python →
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: Medium
⚡ Quick Fix
Store per-instance state in the instance __dict__ (keyed by __set_name__) or a WeakKeyDictionary, never on the shared descriptor object; prefer property for single attributes.
📦 Applies To
python 3.6 library web cli
🔗 Prerequisites
🔍 Detection Hints
def __(set|delete)__\(self, obj[^)]*\):[\s\S]*?self\.\w+\s*=
Auto-detectable: ✗ No
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: Medium ✗ Manual fix Fix: Medium Context: Class Tests: Update


✓ schema.org compliant