DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Daw-Ran Liou
Daw-Ran Liou

Posted on • Updated on

Writing descriptors in Python 3.6+

alt intercept
American football players "blocking" the kick, not "intercepting."

Special thanks to Luciano Ramalho. I learned most of the knowledge about descriptors from his workshop in PyBay 2017

Have you seen this code or maybe have written code like this?

from sqlalchemy import Column, Integer, String
class User(Base):
    id = Column(Integer, primary_key=True)
    name = Column(String)
Enter fullscreen mode Exit fullscreen mode

This code snippet partially comes from the tutorial of a popular ORM package called SQLAlchemy. If you ever wonder why the attributes id and name aren't passed into the __init__ method and bind to the instance like regular class does, this post is for you.

This post starts with explaining descriptors, why to use them, how to write them in previous Python versions (<= 3.5,) and finally writing them in Python 3.6 with the new feature described in PEP 487 -- Simpler customisation of class creation

If you are in a hurry or you just want to know what's new, scroll all the way down to the bottom of this article. You'll find the whole code.

What are descriptors

A great definition of descriptor is explained by Raymond Hettinger in Descriptor HowTo Guide:

In general, a descriptor is an object attribute with β€œbinding behavior”, one whose attribute access has been overridden by methods in the descriptor protocol. Those methods are __get__(), __set__(), and __delete__(). If any of those methods are defined for an object, it is said to be a descriptor.

There are three ways to access an attribute. Let's say we have the a attribute on the object obj:

  1. To lookup its value, some_variable = obj.a,
  2. To change its value, obj.a = 'new value', or
  3. To delete it, del obj.a

Python is dynamic and flexible to allow users intercept the above expression/statement and bind behaviors to them.

Why you want to use descriptors

Let's see an example:

class Order:
    def __init__(self, name, price, quantity):
        self.name = name
        self.price = price
        self.quantity = quantity

    def total(self):
        return self.price * self.quantity

apple_order = Order('apple', 1, 10)
apple_order.total()
# 10
Enter fullscreen mode Exit fullscreen mode

Despite the lack of proper documentation, there is a bug:

apple_order.quantity = -10
apple_order.total
# -10, too good of a deal!
Enter fullscreen mode Exit fullscreen mode

Instead of using getter and setter methods and break the APIs, let's use property to enforce quantity be positive:

class Order:
    def __init__(self, name, price, quantity):
        self._name = name
        self.price = price
        self._quantity = quantity  # (1)

    @property