Python’s descriptors

Nicolas Zermati
Alan Product and Technical Blog
3 min readJun 29, 2022
Pipes to represent descriptor’s “plumbing” — by Samuel Sianipar and found on unsplash

In the previous write-up, I talked about adding temporal information to models. I stayed at the level of Python objects, suggesting adding a EffectivePeriod attribute rather than interval/range/period APIs to the temporal models.

But when we start working with persisted models (ORM), we might need to deal with start_date and end_date directly at the model level. This article shows how descriptors can help us do that easily. Also, going a bit further with descriptors, we’ll rebuild queries helpers (hybrid extensions).

Deriving an attribute

We’re stuck with start_date and end_date on the model. That doesn’t mean we should use them. We could totally add our effective_period to it and use that instead... The challenge is to keep all that in sync. To get that, we can write the following code:

We can do even better by adding a setter to that property:

We can already enjoy access to the model.effective_period, as promised in the intro 🎉

From Property to Descriptor

One small limitation here is when we try to share that behavior. We could put it in a class/mixin and rely on multiple inheritances to make that available across many models. An alternative is to use Python’s descriptors, which is exactly what @property does under the hood...

A descriptor defines how to interact with an object’s attribute. This access can be customized as much as needed as behaviors are described by a plain old python object that can be customized through its __init__ method (not shown above).

Answering __get__ is enough to tell Python that model.effective_period gets its value from DerivedEffectivePeriod.__get__(...)! It’s the same for model.effective_period = ... which will rely on DerivedEffectivePeriod.__set__(...) to customize its behavior.

Identifying the caller

Another nice option descriptors offer: they allow to distinguish between:

  • access from the class: Model.effective_period and
  • access from an instance: model.effective_period.

From there, it’s possible to behave differently depending on who the caller is. This will directly help us to achieve the second objective: to provide a similar feature as hybrid_{property,method}...

We can update the previous descriptor:

This introduces the EffectivePeriodQueryHelper that can build the proper SQL clauses based on the model’s columns.

Again, this is roughly what the hybrid extension does under the hood when declaring an expression: it keeps the expression function within the descriptor and will use it when the hybrid attribute will be called from the class.

We now have a way to organize period logic for SQL generation and to query it from the ORM with:

Conclusion

Sharing code through composition often makes more sense than through inheritance. Python has a unique feature built-in in its execution model: descriptors. They are the foundation for many Python features such as the @property decorator and for third parties libraries like sql-alchemy’s hybrid. Leveraging them gives us more flexibility when we want to share behavior across the codebase when basic composition can’t cut it.

PS: If you have any tricks to type descriptors, and avoid the Union from before, let me know! (💡 @overload was suggested)

Appendix: complete samples

For both the previous article and this one, the supporting code can be found on Github.

--

--