Python descriptors
magic methods
__get__
__set__
Python programming

Understanding __get__ and __set__ and Python descriptors

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Python descriptors are the mechanism behind many attribute access features, including property, bound methods, and framework-managed fields. They let you intercept get and set operations with reusable logic. Understanding __get__ and __set__ gives you a clear model for validation, lazy loading, and attribute-level behavior control.

Descriptor Protocol Fundamentals

An object is a descriptor if it defines one or more of these methods:

  • '__get__(self, instance, owner)'
  • '__set__(self, instance, value)'
  • '__delete__(self, instance)'

When assigned as a class attribute, Python routes attribute access through these methods.

python
1class DebugDescriptor:
2    def __get__(self, instance, owner):
3        print("__get__ called", instance, owner)
4        return "descriptor value"
5
6
7class Demo:
8    field = DebugDescriptor()
9
10
11d = Demo()
12print(d.field)

This shows how reading d.field invokes descriptor logic instead of direct dictionary lookup.

Data Versus Non-Data Descriptors

Descriptor precedence depends on whether __set__ or __delete__ exists.

  • data descriptor: defines __set__ or __delete__
  • non-data descriptor: defines only __get__

Data descriptors override instance dictionary values. Non-data descriptors can be shadowed by instance attributes.

python
1class NonData:
2    def __get__(self, instance, owner):
3        return "from non-data"
4
5
6class Data:
7    def __get__(self, instance, owner):
8        return instance.__dict__.get("_x", None)
9
10    def __set__(self, instance, value):
11        instance.__dict__["_x"] = value
12
13
14class Example:
15    a = NonData()
16    b = Data()
17
18
19e = Example()
20e.a = "instance override"
21e.b = 10
22
23print(e.a)
24print(e.b)

This precedence rule explains many surprising attribute behaviors.

Build a Validation Descriptor

Descriptors are useful for reusable validation rules.

python
1class PositiveInt:
2    def __set_name__(self, owner, name):
3        self.storage_name = "_" + name
4
5    def __get__(self, instance, owner):
6        if instance is None:
7            return self
8        return getattr(instance, self.storage_name)
9
10    def __set__(self, instance, value):
11        if not isinstance(value, int) or value <= 0:
12            raise ValueError("value must be a positive integer")
13        setattr(instance, self.storage_name, value)
14
15
16class Product:
17    quantity = PositiveInt()
18
19    def __init__(self, quantity):
20        self.quantity = quantity
21
22
23p = Product(5)
24print(p.quantity)

__set_name__ gives each descriptor instance the attribute name it was assigned to.

Relationship to property

property is implemented with descriptors. Getter and setter decorators attach methods that act like __get__ and __set__ behavior.

python
1class User:
2    def __init__(self):
3        self._age = 0
4
5    @property
6    def age(self):
7        return self._age
8
9    @age.setter
10    def age(self, value):
11        if value < 0:
12            raise ValueError("age must be non-negative")
13        self._age = value

Understanding descriptor protocol makes property feel less magical and easier to debug.

Attribute Lookup Order Overview

For obj.attr, Python roughly checks:

  1. data descriptor in class
  2. instance dictionary
  3. non-data descriptor in class
  4. regular class attribute
  5. base classes by method resolution order

This lookup order is the practical foundation for debugging descriptor conflicts.

Framework Usage Patterns

Descriptors appear in many frameworks:

  • ORM fields that validate or lazily fetch values
  • settings systems with typed conversion
  • caching proxies that compute on first access

When you see framework attributes acting like methods or smart fields, descriptors are often involved.

Debugging Descriptor Problems

Practical debugging steps:

  • inspect type(obj).__dict__ to see descriptor objects
  • print descriptor methods during access
  • check instance __dict__ for shadowing
  • verify data versus non-data descriptor behavior

These checks usually reveal precedence mistakes quickly.

Common Pitfalls

A common pitfall is storing per-instance state on the descriptor object itself, which leaks values between instances. Another is forgetting to handle instance is None in __get__, which breaks class-level access. Developers also expect non-data descriptors to override instance attributes, which they do not. Missing __set_name__ usage can cause brittle storage naming. Finally, vague validation errors inside descriptors make debugging harder than needed.

Summary

  • Descriptors customize attribute access via __get__, __set__, and __delete__.
  • Data descriptors take precedence over instance dictionary values.
  • 'property is a descriptor-based abstraction.'
  • Descriptors are excellent for reusable validation and lazy behavior.
  • Lookup-order understanding is essential for debugging attribute conflicts.
  • Clear state handling and error messages make descriptor code maintainable.

Course illustration
Course illustration

All Rights Reserved.