Python

Python Descriptor Protocol Explained Simply

Learn how Python's descriptor protocol powers properties, methods, and reusable attribute behavior. Understand `__get__`, `__set__`, and `__delete__` with clear examples.

August 2026 8 min read 13 views 0 hearts

Python’s Descriptor Protocol Explained – The Magic Behind Properties and Methods

If you’ve been using Python for a while, you’ve probably taken classes, properties, and methods for granted. But have you ever wondered what makes @property work? Or why a method automatically receives self when you call it on an instance? The secret lies in Python’s descriptor protocol. It’s one of those things that feels magical until you understand it. And once you do, it unlocks a whole new level of control over how objects behave.

Let’s break it down without the jargon—just clear, practical examples.

What Exactly is a Descriptor?

Simply put, a descriptor is an object that defines how attribute access works on another object. An attribute can be looked up (__get__), set (__set__), or deleted (__delete__). When you define any of these three methods in a class, you turn that class into a descriptor.

Here’s the simplest descriptor you’ll ever see:

class MyDescriptor:
    def __get__(self, obj, objtype=None):
        return "You just read this"

If you place an instance of MyDescriptor as a class attribute, accessing that attribute on an instance will call our __get__ method.

The Three Methods That Make It Work

Method Purpose Example
__get__(self, obj, objtype) Called when you read the attribute instance.attr
__set__(self, obj, value) Called when you assign to it instance.attr = 5
__delete__(self, obj) Called when you delete it del instance.attr

Not all three are required. A descriptor that only implements __get__ is called a non-data descriptor. If it implements __set__ or __delete__, it becomes a data descriptor. This distinction matters because data descriptors take priority over instance attributes in lookup order.

A Real Example: Validated Attributes

Imagine you want a class where a certain attribute must always be a positive integer. Without descriptors, you’d pollute your code with validation logic everywhere. With descriptors, it’s clean.

class PositiveInt:
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, objtype=None):
        return obj.__dict__.get(self.name, 0)

    def __set__(self, obj, value):
        if not isinstance(value, int) or value <= 0:
            raise ValueError(f"{self.name} must be a positive integer")
        obj.__dict__[self.name] = value

Now you can use it in any class:

class Product:
    price = PositiveInt()

p = Product()
p.price = 10   # Works
p.price = -5   # Raises ValueError

This pattern is how frameworks like Django or SQLAlchemy do field validation under the hood. PythonSkillset users often build similar validators for configuration classes.

Why Properties and Methods Are Descriptors

The @property decorator is actually a convenience wrapper around the descriptor protocol. When you write:

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def area(self):
        return 3.14 * self._radius ** 2

Python turns area into a descriptor with only a __get__ method. That’s why circle.area computes on the fly.

Methods are also descriptors. When you call instance.method(), Python actually does:

  1. Look up method on the class – finds a function object.
  2. That function’s __get__ method is called, binding the instance to the method.
  3. The result is a bound method that already has self filled in.

How Lookup Order Works

When you access an attribute on an instance, Python follows this order:

  1. Data descriptors on the class hierarchy (those with __set__ or __delete__).
  2. Instance attributes (stored in obj.__dict__).
  3. Non-data descriptors and other class attributes.

This is why you can override a property by assigning to the instance directly, but not a data descriptor. It’s a subtle but important detail when designing APIs.

When Should You Use Descriptors?

Descriptors shine when you need reusable attribute behavior. Common use cases include:

  • Validation (type checking, range checks)
  • Lazy loading (compute expensive values only when accessed)
  • Logging access (audit trails for sensitive data)
  • Implementing ORM fields

At PythonSkillset, we’ve seen descriptors used to build mini-frameworks for configuration systems, where each setting has its own validation, default, and change tracking.

A Word of Caution

Don’t reach for descriptors too quickly. They add complexity, and often a simple @property or even a regular method is enough. But when you find yourself repeating the same attribute logic across many classes, that’s your cue. Descriptors let you write that logic once and reuse it cleanly.

Next Steps

Play with the code. Create a descriptor that logs every read or write to a file. Then try building one that caches expensive computations. Descriptors are one of those Python features that feel like a superpower once you understand them. And the best part? You’ve been using them all along without knowing.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.