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