Q&A
What is the @property in Python?
Overview
Python's philosophy is to start with plain attributes and only add property logic when genuinely needed - no Java-style getters/setters for every field. When you do need controlled access (validation, computed values, backwards compatibility), @property upgrades a plain attribute into a managed one while keeping the call syntax clean.
graph TD
A["@property → getter"] --> B["obj.name reads the value"]
C["@name.setter → setter"] --> D["obj.name = value triggers validation"]
E["@name.deleter → deleter"] --> F["del obj.name runs cleanup"]
B & D & F --> G["All accessed like plain attributes - no ()"]
style A fill:#1e1b4b,color:#fff
Read-Only Property (getter only)
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
"""Computed on-the-fly - no stored area field."""
return 3.14159 * self._radius ** 2
c = Circle(5)
print(c.area) # 78.53975 - called like an attribute, no parentheses
c.area = 99 # AttributeError: can't set attribute - read-only
This is a computed property: area is derived from radius and never stored. Because only the getter is defined, it's read-only.
Full Property with Getter + Setter + Validation
class Temperature:
def __init__(self, celsius):
self.celsius = celsius # triggers the setter immediately
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError(f"Temperature below absolute zero: {value}")
self._celsius = value # store in a "private" backing variable
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
t = Temperature(100)
print(t.fahrenheit) # 212.0
t.celsius = -300 # ValueError: Temperature below absolute zero
The setter runs whenever obj.celsius = ... is assigned - validation is automatic and centralized. The backing variable _celsius (convention: single underscore = "internal") avoids infinite recursion between the property and itself.
Deleter
@celsius.deleter
def celsius(self):
print("Deleting temperature")
del self._celsius
del t.celsius # prints "Deleting temperature" and removes _celsius
Backwards Compatibility Pattern
The most important use case in practice: you start with a plain attribute and later need to add validation or computation without breaking all existing code that reads/writes obj.name:
# v1 - plain attribute
class User:
def __init__(self, name):
self.name = name # direct attribute
# v2 - add validation; same public API
class User:
def __init__(self, name):
self.name = name # this now calls the setter
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not value.strip():
raise ValueError("Name cannot be blank")
self._name = value.strip()
All existing code (user.name, user.name = "...") continues to work unchanged.
@property vs Plain Attribute
- Use a plain attribute for simple, mutable data with no constraints.
- Use
@propertywhen you need: validation, computed/derived values, lazy initialization, or to maintain backwards compatibility when adding logic to an existing attribute. - Do NOT add properties for every field "just in case" - Python is not Java. Start plain; add
@propertyonly when logic is needed.
Why This Matters in Interviews
Python OOP interviews use @property to test encapsulation idioms. Key points: it turns a method into an attribute-style access (no ()), the getter/setter/deleter trio, validation in the setter, computed properties in the getter, and - the most important pattern - adding logic to a plain attribute without changing the public API (backwards compatibility). Knowing to use a backing _var to avoid infinite recursion, and explaining the Pythonic "don't add getters/setters preemptively" philosophy, signals maturity.
Common Pitfalls
- Storing the value under the same name as the property (e.g.
self.celsius = valueinsidecelsius.setter) → infinite recursion (the setter calls itself). Useself._celsiusorself.__celsius. - Forgetting
@property.setteruses the same name as the property (e.g.@celsius.setter, not@property.setter). - Adding
@propertygetters/setters for every field (un-Pythonic; do it only when logic is needed). - Not calling
super().__init__()in subclasses that override a property, missing parent initialization.
References
- Python
property(): https://docs.python.org/3/library/functions.html#property - Python descriptor how-to: https://docs.python.org/3/howto/descriptor.html