Skip to content
advanced Phase 17 · Python Metaprogramming

Descriptors & Properties

Implement descriptors for custom attribute access and validation.

1h
0 problems
Topic Progress 0%

The Attribute Lookup Protocol

The Attribute Lookup Protocol

Every obj.attr access follows one order — knowing it explains most attribute mysteries:

  1. Data descriptor on the type (defines __get__ and __set__)
  2. Instance __dict__ entry for attr
  3. Non-data descriptor or class attribute (only __get__, or a plain value)
  4. AttributeError
class D:                      # data descriptor — wins over instance dict
    def __get__(self, obj, owner=None): return 0
    def __set__(self, obj, value): pass

class ND:                     # non-data descriptor — instance dict wins
    def __get__(self, obj, owner=None): return 0

class C:
    d, nd = D(), ND()

Methods are non-data descriptors bound at lookup time; property, classmethod, and staticmethod also participate. super().attr and obj.__dict__ peek at the layers underneath.

Building Reusable Descriptors

Building Reusable Descriptors

class Validated:
    def __init__(self, kind):
        self.kind = kind

    def __set_name__(self, owner, name):     # 3.6+: receives the attribute name
        self.name = name

    def __get__(self, obj, owner=None):
        if obj is None:
            return self                      # accessed on the class
        return obj.__dict__.get(self.name, self.kind())

    def __set__(self, obj, value):
        if not isinstance(value, self.kind):
            raise TypeError(f"{self.name} expects {self.kind.__name__}")
        obj.__dict__[self.name] = value      # delegate storage to the instance

class Money:
    cents = Validated(int)

m = Money(); m.cents = 100

One descriptor validates fields across many classes — the pattern behind ORM fields (django.db.models.CharField). __set_name__ avoids passing the name twice.

Properties, Pitfalls, and Interviews

Properties, Pitfalls, and Interviews

property is just the stdlib data descriptor for getter/setter/deleter triples:

class Temp:
    def __init__(self, c): self._c = c
    @property
    def f(self): return self._c * 9 / 5 + 32

Pitfalls:

  • Non-data descriptors are shadowable. If your class defines only __get__, an instance attribute of the same name silently overrides it — usually a bug, occasionally a feature (caching a bound method)
  • Missing __set__ flips the descriptor class; store values where you intend
  • property on the class, not the instance: t.f.getter(...) fails — t.f already invoked __get__
  • slots are type-created descriptors; unknown attributes raise AttributeError

Interview angle: recite the 4-step lookup order, then explain how caching stores into obj.__dict__ while the descriptor keeps first lookup.

Quiz

1. What makes a descriptor a "data" descriptor?

Question 1 options

2. Why does instance.__dict__ sometimes win over a class descriptor?

Question 2 options

3. What does __set_name__ provide?

Question 3 options

Flashcards

Question

Data vs non-data descriptor

Answer

Data = defines __set__ (beats instance dict); non-data = __get__ only (loses to instance dict).

Question

property

Answer

Stdlib data descriptor wrapping getter/setter/deleter functions into one attribute.

Question

__set_name__

Answer

Hook receiving the attribute name at class-creation time for self-registering fields.

Question

Attribute lookup order

Answer

type data descriptor → instance __dict__ → non-data descriptor/class attr → AttributeError.

Revision Notes

Key Takeaways

  • 1. Attribute access is a 4-step protocol; most "why is my value ignored" bugs are ordering issues
  • 2. Descriptors package reusable validation/storage logic at the type level
  • 3. property covers one-class cases; hand-written descriptors cover patterns repeated across classes

Interview Tips

  • • State the lookup order precisely and name one consequence of each position
  • • Use the ORM-field example to connect descriptors to real frameworks interviewers know

Cheat Sheet

lookup: data descriptor > instance dict > non-data descriptor; get/set/delete + set_name; property = built-in data descriptor.

Continue Learning