Descriptors & Properties

15 questions found

What does the @property decorator do, and how does it let a method be accessed like an attribute?

Beginner
@property turns a method into a computed, READ-ONLY (by default) attribute — calling it WITHOUT parentheses runs the underlying method and returns its result, letting you expose computed values with clean attribute syntax instead of requiring an explicit method call.
class Circle:
    def __init__(self, radius):
        self.radius = radius
    @property
    def area(self):
        return 3.14159 * self.radius ** 2

c = Circle(5)
print(c.area)  # 78.53975 -- accessed like an attribute, no parentheses needed
Real-world example Exposing a computed value (like area, or a full_name derived from first_name/last_name) as if it were a plain attribute.

Common follow-ups: How would you also allow SETTING a value through a property, using the @x.setter decorator?

Magic Methods & Operator Overloading

How do you add a setter to a property so assignment (obj.attr = value) triggers custom logic, like validation?

Beginner
Define a second method with the SAME name, decorated with @property_name.setter — this method runs whenever someone assigns to the property, letting you validate or transform the incoming value before storing it (typically in a differently-named private attribute).
class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius
    @property
    def celsius(self):
        return self._celsius
    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Below absolute zero!")
        self._celsius = value

t = Temperature(20)
t.celsius = 25  # runs the setter's validation logic
Real-world example Validating that a value assigned to an attribute (like temperature or age) meets certain constraints before accepting it.

Common follow-ups: Why does the underlying stored value typically need a DIFFERENT name (like _celsius) from the property itself (celsius)?

Exception Handling

What is a descriptor, and which dunder methods define the descriptor protocol?

Intermediate
A descriptor is any object implementing __get__, __set__, and/or __delete__, placed as a CLASS attribute — when accessed through an instance, Python automatically calls these methods instead of doing a plain attribute lookup, giving you full control over get/set/delete behavior; @property is actually implemented USING descriptors under the hood.
class PositiveNumber:
    def __set_name__(self, owner, name):
        self.name = f"_{name}"
    def __get__(self, instance, owner):
        return getattr(instance, self.name)
    def __set__(self, instance, value):
        if value < 0:
            raise ValueError("Must be positive")
        setattr(instance, self.name, value)

class Product:
    price = PositiveNumber()  # a reusable descriptor
Real-world example Building a REUSABLE validation rule (like PositiveNumber) that can be applied to multiple attributes across different classes.

Common follow-ups: What's the practical advantage of a reusable descriptor class over writing a separate @property for each validated attribute?

Magic Methods & Operator Overloading

What is the difference between a data descriptor and a non-data descriptor, and how does this affect attribute lookup priority?

Intermediate
A DATA descriptor implements BOTH __get__ AND __set__ (or __delete__); a NON-DATA descriptor implements ONLY __get__. Data descriptors take priority OVER an instance's __dict__ during attribute lookup, while non-data descriptors are OVERRIDDEN by an instance's __dict__ if the same name exists there — this priority difference is exactly how @property (a data descriptor) always wins even if you try to shadow it in self.__dict__.
class NonDataDescriptor:
    def __get__(self, instance, owner):
        return "from descriptor"

class Example:
    attr = NonDataDescriptor()

e = Example()
e.__dict__["attr"] = "from instance dict"
print(e.attr)  # 'from instance dict' -- instance __dict__ wins for non-data descriptors
Real-world example Understanding why a @property (data descriptor) can't be silently overridden by setting self.__dict__['prop_name'] directly.

Common follow-ups: Why does a functions themselves (used for bound methods) behave as a non-data descriptor?

Magic Methods & Operator Overloading

How does __set_name__ let a descriptor automatically learn the attribute name it's being assigned to on the owning class?

Intermediate
__set_name__(self, owner, name) is automatically called by Python ONCE, at CLASS CREATION time, passing the owning class and the exact attribute name the descriptor was assigned to — letting a reusable descriptor derive a private storage attribute name (like '_price') without the user having to pass it explicitly.
class Validated:
    def __set_name__(self, owner, name):
        self.private_name = f"_{name}"  # automatically learns its own name
    def __get__(self, instance, owner):
        return getattr(instance, self.private_name)
    def __set__(self, instance, value):
        setattr(instance, self.private_name, value)

class Product:
    price = Validated()  # __set_name__ automatically learns 'price' -> '_price'
Real-world example Building a reusable descriptor class that automatically derives its own storage attribute name without manual configuration.

Common follow-ups: Before __set_name__ was added (Python 3.6+), how did descriptors typically need the attribute name passed to them?

Magic Methods & Operator Overloading

How would you implement a lazy, cached property using a custom descriptor (or functools.cached_property) that only computes its value once?

Advanced
functools.cached_property computes the decorated method's value on FIRST access, then STORES the result directly in the instance's __dict__ (overriding further descriptor-based lookups for that name, since instance __dict__ takes priority for non-data descriptors) — subsequent accesses skip recomputation entirely, unlike @property which recomputes every single time.
from functools import cached_property

class DataProcessor:
    def __init__(self, data):
        self.data = data
    @cached_property
    def expensive_summary(self):
        print("Computing...")
        return sum(self.data) / len(self.data)

dp = DataProcessor([1, 2, 3, 4, 5])
print(dp.expensive_summary)  # 'Computing...' printed, then 3.0
print(dp.expensive_summary)  # 3.0, NO 'Computing...' -- cached from __dict__
Real-world example Caching an expensive, rarely-changing computed value (like a parsed configuration or aggregated statistic) on first access.

Common follow-ups: Why does cached_property specifically require the CLASS to NOT use __slots__, unlike a regular @property?

Memory Management & Garbage Collection

How do you write a descriptor that behaves differently when accessed on the CLASS itself versus on an INSTANCE?

Advanced
__get__(self, instance, owner) receives 'instance' as None when accessed via the CLASS (e.g., MyClass.attr) rather than an instance (e.g., my_obj.attr) — checking `if instance is None` inside __get__ lets you return something different (often the descriptor itself, matching @property's own convention) for class-level access.
class Descriptor:
    def __get__(self, instance, owner):
        if instance is None:
            return self  # accessed via the class itself: return the descriptor
        return f"Value for {instance}"

class Example:
    attr = Descriptor()

print(Example.attr)          # <Descriptor object> -- instance is None here
print(Example().attr)        # "Value for <Example object>" -- instance is set
Real-world example Building a descriptor (like an ORM field definition) that exposes metadata when accessed via the class, but returns actual data via an instance.

Common follow-ups: How does Django's ORM use exactly this class-vs-instance distinction to let Model.field_name work for QUERYING?

OOP

How would you implement a type-validating descriptor that enforces a specific type constraint, reusable across multiple attributes and classes?

Advanced
Build a generic descriptor class parameterized by the expected type (stored in __init__), and raise a TypeError inside __set__ if the assigned value doesn't match — combined with __set_name__ for automatic private-attribute naming, this gives you a fully reusable, DRY validation mechanism applicable to any class.
class Typed:
    def __init__(self, expected_type):
        self.expected_type = expected_type
    def __set_name__(self, owner, name):
        self.name = f"_{name}"
    def __get__(self, instance, owner):
        return getattr(instance, self.name, None)
    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f"Expected {self.expected_type}, got {type(value)}")
        setattr(instance, self.name, value)

class User:
    name = Typed(str)
    age = Typed(int)
Real-world example Building a reusable, DRY type-validation mechanism for class attributes without repeating validation logic in every __init__ or property setter.

Common follow-ups: How does this hand-rolled Typed descriptor conceptually relate to what a library like pydantic does more comprehensively?

Type Hints

How does Python's own implementation of instance methods use the descriptor protocol to achieve automatic 'self' binding?

Advanced
Functions are non-data descriptors — a plain function's __get__ method, when accessed through an INSTANCE, returns a BOUND METHOD object with 'self' automatically pre-filled as the first argument; accessed through the CLASS directly, it returns the plain, unbound function — this is the exact mechanism underlying Python's automatic method binding.
class Example:
    def greet(self):
        return "Hello!"

e = Example()
print(Example.greet)     # <function Example.greet at ...> -- unbound, plain function
print(e.greet)           # <bound method Example.greet of <Example object>> -- 'self' auto-bound
print(e.greet())         # "Hello!" -- self was automatically supplied
Real-world example Understanding the actual mechanism (function.__get__) that makes 'self' automatically get passed to instance methods without you doing it manually.

Common follow-ups: Would a staticmethod-wrapped function behave the same way when accessed through an instance, or differently?

Magic Methods & Operator Overloading

How do you implement a descriptor-based ORM-style field that lazily loads related data only when actually accessed?

Advanced
Implement __get__ to check if the related data has already been fetched and cached on the instance; if not, perform the (potentially expensive) lookup, cache the result, and return it — this pattern underlies 'lazy loading' relationship fields in ORMs like Django and SQLAlchemy, deferring expensive queries until genuinely needed.
class LazyForeignKey:
    def __set_name__(self, owner, name):
        self.cache_name = f"_{name}_cache"
    def __get__(self, instance, owner):
        if instance is None:
            return self
        if not hasattr(instance, self.cache_name):
            related_id = getattr(instance, f"{self.name}_id")
            setattr(instance, self.cache_name, fetch_related_object(related_id))  # lazy DB query
        return getattr(instance, self.cache_name)
Real-world example Implementing a lazy-loaded relationship field (like Django's ForeignKey) that only queries the database when the related object is actually accessed.

Common follow-ups: What's the downside (N+1 query risk) of this lazy-loading pattern when accessed in a loop over many instances?

Debugging & Profiling

Showing 1–10 of 15