Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Python

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.

July 2026 6 min read 2 views 0 hearts

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.

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.