Back to Blog
Python

Python Descriptor vs Decorator: What's the Difference?

python descriptor vs decorator: Understand the difference between Python descriptors and decorators, how they work, and when to use each in your code.

descriptorsdecoratorspythonmetaprogrammingattribute access
Illustration comparing Python descriptor and decorator concepts

The terms descriptor and decorator are easy to confuse because they both appear in Python code that modifies behavior. But they solve different problems: a decorator wraps a function or class to change its behavior, while a descriptor is a protocol that controls how an attribute is accessed on an instance. This article clarifies the difference between python descriptor vs decorator, shows how each works, and explains when you would use one over the other.

Why Descriptors and Decorators Get Mixed Up

The confusion usually starts with @property. It looks like a decorator, and you apply it to a method. But property is actually a descriptor class. When you write @property, you are using the descriptor as a decorator. This overlap makes it seem like descriptors and decorators are interchangeable, but they are not.

A decorator is a callable that takes a function or class and returns a new one. A descriptor is an object that implements at least one of __get__, __set__, or __delete__. The descriptor protocol is what makes attribute access on an instance go through those methods. Decorators operate at definition time; descriptors operate at attribute access time.

What a Decorator Actually Does

A decorator is a function that wraps another function or class. The classic example is a timing decorator:

import time def timer(func): def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) end = time.perf_counter() print(f"{func.__name__} took {end - start:.6f} seconds") return result return wrapper @timer def slow_add(a, b): time.sleep(0.1) return a + b

The @timer syntax is equivalent to slow_add = timer(slow_add). The decorator receives the original function and returns a new function that adds behavior. Decorators can also be applied to classes, and they can accept arguments if you create a decorator factory.

Decorators are useful for cross-cutting concerns like logging, caching, access control, and validation. They are a way to reuse code that modifies callables without changing their source.

What a Descriptor Actually Is

A descriptor is a class that defines one or more of the special methods __get__, __set__, or __delete__. When an attribute on an instance is looked up, Python checks if the attribute's class has these methods. If it does, the descriptor protocol kicks in.

Here is a minimal descriptor that logs every access to an attribute:

class LoggedAccess: def __init__(self, name): self.name = name def __get__(self, obj, objtype=None): print(f"Accessing {self.name}") return obj.__dict__.get(self.name) def __set__(self, obj, value): print(f"Setting {self.name} to {value}") obj.__dict__[self.name] = value class Point: x = LoggedAccess("x") y = LoggedAccess("y") p = Point() p.x = 3 # prints "Setting x to 3" print(p.x) # prints "Accessing x" then 3

When you assign p.x = 3, Python calls LoggedAccess.__set__(p, 3). When you read p.x, it calls LoggedAccess.__get__(p, Point). The descriptor controls how the attribute is stored and retrieved. Without __set__, the descriptor is non-data and only affects reads.

Descriptors are the mechanism behind property, classmethod, staticmethod, and slots. They allow you to define reusable attribute behavior that can be applied to many classes.

How They Interact: Descriptors as Decorators

The most common intersection is property. property is a descriptor class, but it is also designed to be used as a decorator:

class Circle: def __init__(self, radius): self._radius = radius @property def radius(self): return self._radius @radius.setter def radius(self, value): if value < 0: raise ValueError("Radius cannot be negative") self._radius = value

Here, @property takes the radius method and returns a descriptor object. The @radius.setter decorator then adds a setter to that descriptor. This works because property implements the descriptor protocol, and its methods return new descriptor objects when used as decorators.

You can also create your own descriptor that is meant to be used as a decorator. For example, a descriptor that validates a value:

class PositiveNumber: def __init__(self, func): self.func = func self.name = func.__name__ def __get__(self, obj, objtype=None): if obj is None: return self return obj.__dict__.get(self.name) def __set__(self, obj, value): if value <= 0: raise ValueError(f"{self.name} must be positive") obj.__dict__[self.name] = value class Order: def __init__(self, quantity): self.quantity = quantity @PositiveNumber def quantity(self): return self._quantity

In this pattern, the descriptor's __init__ receives the function it decorates. The descriptor stores the function but does not call it; instead, it uses the function's name as the attribute key. This is a valid way to combine the two concepts, but it is not the typical use case.

Choosing Between a Decorator and a Descriptor

The decision comes down to what you are trying to control.

Use a decorator when you want to modify the behavior of a function or class at definition time. Decorators are ideal for adding side effects like logging, timing, or memoization. They are simple to write and understand, and they work on any callable.

Use a descriptor when you need to control attribute access on instances. Descriptors give you fine-grained control over reads, writes, and deletes. They are essential for implementing properties, class methods, and static methods. Descriptors are also reusable: you can define one descriptor class and apply it to many attributes across different classes.

There is a gray area where you might use a descriptor as a decorator, as shown above. This is usually reserved for cases where the attribute behavior is tightly coupled to a method definition. For most code, you will not need to write your own descriptors. The built-in property, classmethod, and staticmethod cover the common needs.

AspectDecoratorDescriptor
Primary purposeModify function/class behaviorControl attribute access
When it runsAt definition timeAt attribute access time
Syntax@decoratorDefine class with __get__/__set__
Common examples@lru_cache, @app.routeproperty, classmethod, staticmethod
ReusabilityOften one-offDesigned for reuse
Runtime costCall overhead when wrapped function is called__get__/__set__ called on every attribute access

Runtime Behavior and Performance

Descriptors add overhead to every attribute access. When you read obj.attr, Python has to look up the descriptor and call its __get__ method. This is slower than a plain attribute lookup. If you have a hot loop that accesses an attribute millions of times, a descriptor can become a bottleneck. The same is true for property, which is a descriptor.

Decorators, on the other hand, add overhead only when the wrapped function is called. The wrapper function itself adds a frame and a function call. For most applications this is negligible, but in tight loops it can matter.

If performance is critical, measure before optimizing. Sometimes you can replace a descriptor with a simple attribute and move validation to a method. But for most code, the clarity and safety provided by descriptors outweigh the small performance cost.

Common Pitfalls and How to Avoid Them

One common mistake is forgetting that a data descriptor (one with __set__) takes precedence over the instance dictionary. If you try to set an attribute that is a data descriptor, __set__ is called, and the value is not stored in __dict__ unless you do it manually. This can lead to confusing behavior if you expect the descriptor to be bypassed.

Another pitfall is using a descriptor without __set_name__. In Python 3.6+, you can implement __set_name__ to automatically receive the attribute name when the class is created. Without it, you have to pass the name manually, which is error-prone.

class Descriptor: def __set_name__(self, owner, name): self.name = name class MyClass: attr = Descriptor()

This is cleaner than passing the name in the constructor. Always use __set_name__ when your descriptor needs to know the attribute name.

Finally, do not confuse decorators with descriptors in documentation or code reviews. They are orthogonal concepts. A decorator can return a descriptor, and a descriptor can be used as a decorator, but they are not synonyms. Understanding the distinction will help you write more maintainable and predictable Python code.

python descriptor vs decorator: Practical Usage and Code Exa | RYUSLOG DEV