When to Use Python's property() vs. Descriptors
Learn when to choose Python's @property decorator versus defining full descriptors for reusable attribute logic. This guide compares both approaches with real-world examples and practical rules for avoiding over-engineering.
You've probably used @property dozens of times without thinking much about it. It's clean, it's Pythonic, and it just works. But then you run into a situation where you need the same getter/setter logic across multiple attributes, and suddenly you're copy-pasting the same decorator pattern over and over again. That's when descriptors come into play.
Let me walk you through the real differences and when each approach makes sense in your day-to-day work at PythonSkillset.
The Quick Rule of Thumb
Use property() when:
- You need to validate or transform a single attribute
- The logic is simple and specific to that one attribute
- You don't want to over-engineer things
Use descriptors when: - You need the same behavior across multiple attributes or classes - You're building a framework or reusable component - The logic is complex enough to warrant its own class
Understanding property() First
The @property decorator is essentially a shortcut for creating descriptors. When you write:
class User:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name.title()
@name.setter
def name(self, value):
if not isinstance(value, str):
raise ValueError("Name must be a string")
self._name = value
You're actually creating a property descriptor behind the scenes. Python's property() is a built-in descriptor that handles the __get__, __set__, and __delete__ methods for you. It's perfect for simple cases like this.
When Descriptors Shine
Now imagine you're building a data validation system. You have multiple attributes that all need the same validation logic:
class PositiveNumber:
def __set_name__(self, owner, name):
self._name = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self._name, 0)
def __set__(self, obj, value):
if not isinstance(value, (int, float)):
raise TypeError(f"{self._name[1:]} must be a number")
if value <= 0:
raise ValueError(f"{self._name[1:]} must be positive")
setattr(obj, self._name, value)
class Order:
quantity = PositiveNumber()
price = PositiveNumber()
discount = PositiveNumber()
def __init__(self, quantity, price, discount=0):
self.quantity = quantity
self.price = price
self.discount = discount
Without descriptors, you'd need to write property definitions for quantity, price, and discount separately, repeating the same logic three times. With descriptors, you write the logic once and reuse it everywhere.
Real-World Example from PythonSkillset
Let me show you why this matters in practice. At PythonSkillset, we had a situation where we needed to track attribute access for debugging purposes across an entire model system.
With property() it would look like this:
class User:
@property
def name(self):
print(f"Accessing name")
return self._name
@property
def email(self):
print(f"Accessing email")
return self._email
# Repeat for every attribute...
With descriptors, we can do it cleanly:
class TrackedAttribute:
def __set_name__(self, owner, name):
self._name = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
print(f"Accessing {self._name[1:]}")
return getattr(obj, self._name)
def __set__(self, obj, value):
print(f"Setting {self._name[1:]} to {value}")
setattr(obj, self._name, value)
class User:
name = TrackedAttribute()
email = TrackedAttribute()
role = TrackedAttribute()
The Hidden Cost of Over-Engineering
But let me be honest with you. Descriptors can be overkill for simple cases. I've seen teams waste hours building descriptor frameworks when a simple property would have worked perfectly fine.
Ask yourself: - Am I writing this descriptor to solve a real problem, or just to be clever? - Will this logic genuinely be reused, or is it a one-off? - Does the team understand descriptor protocol well enough to maintain this?
When to Stick with Property
Stick with @property when:
- You're dealing with one-off attribute logic
- The validation is simple (type checking, range validation)
- You need to make a simple computed attribute read-only
- You're the only developer working on this code
The beauty of property is its simplicity. Any Python developer can look at it and understand what's happening immediately.
When to Reach for Descriptors
Reach for descriptors when:
- You're building a library or framework that others will use
- You have the same pattern appearing in three or more places
- You need class-level configuration (like __set_name__)
- You're implementing lazy loading, caching, or ORM-like behavior
Python's Django ORM and SQLAlchemy both use descriptors extensively. When you define a model field like name = CharField(max_length=100), that's a descriptor doing the heavy lifting behind the scenes.
The Bottom Line
The choice between property() and descriptors comes down to one thing: reusability. If you need the same behavior once, use property. If you need it across multiple attributes or classes, use descriptors.
And here's the thing - you can always refactor. Start with property, and if you find yourself copy-pasting the same pattern a third time, that's your signal to extract it into a descriptor. Don't try to predict the future. Code for what you know now, not what you might need someday.
At PythonSkillset, we follow this principle religiously. Our codebase has a mix of both, and knowing when to use each has saved us countless hours of maintenance and debugging.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.