A mental model of Python descriptors

Because who doesn't like a good mental model?

1 March 2022

The descriptors in Python are a bit arcane. There's magic going on underneath to make them work, so it helps to have a mental model of their interactions before diving into development. I'll start with a nice image and then explain it in the remainder of the post.

descriptor-model

A descriptor is an object that attaches to the attributes of another class. It is critical to understand that this occurs at the class level. Only one descriptor object will be instantiated per class attribute. Even if you instantiate multiple objects from this class, only a single descriptor is used. This behavior is not unique to descriptors, it's similar to any other class attribute. They are only evaluated once when Python loads the module, so it makes sense that you only get a single descriptor.

class MyDescriptor:

  def __init__(self):
    self.value = 0

  def __get__(self, obj, objtype=None):
    return self.value

  def __set__(self, obj, value):
    self.value = value

class MyClass:
  a = MyDescriptor()

# create two different object
# but they share the descriptor
obj1 = MyClass()
obj2 = MyClass()

obj2.a = 5          # set value in the descriptor to 5
assert obj1.a == 5  # get value from descriptor, also 5

In practice, this means that data stored in the descriptor will essentially be shared across the objects. The above code sample shows an example of this happening. This can be a bit of a surprise if you were expecting descriptors to attach at the object level. This behavior might work well in achieving a singleton-like approach. However, most of the time we want the descriptor to maintain state at a per-object level.

Some follow-up questions to explore in future posts ...

  • why should I use descriptors
  • what pattern can I use to maintain per-object data descriptors
  • where do these show up in the wild