property() is Python’s built-in decorator and function for turning class methods into elegant, read-only or read-write properties — giving attribute-like access while allowing getter, setter, and deleter logic. In 2026, @property remains the gold standard for clean encapsulation in data science (pandas-like computed columns, data validation), software engineering (immutable interfaces, lazy evaluation), and modern frameworks (FastAPI models, Pydantic, dataclasses) — enabling computed attributes, validation, caching, and backward-compatible API changes without breaking dot-notation usage.
Here’s a complete, practical guide to using property() in Python: basic getter/setter/deleter, real-world patterns (earthquake data validation, lazy computation, unit conversion), and modern best practices with type hints, caching, descriptors, and integration with dataclasses/Pydantic/Polars/Dask.
Basic @property — read-only property with getter.
class Earthquake:
def __init__(self, mag, depth):
self._mag = mag
self._depth = depth
@property
def mag(self):
return self._mag
@property
def depth(self):
return self._depth
eq = Earthquake(7.2, 25.0)
print(eq.mag) # 7.2 (looks like attribute, calls getter)
# eq.mag = 8.0 # AttributeError: can't set attribute
Full property with getter, setter, deleter — validation & side effects.
class Earthquake:
def __init__(self, mag=0.0, depth=0.0):
self._mag = None
self._depth = None
self.mag = mag # use setter
self.depth = depth # use setter
@property
def mag(self):
return self._mag
@mag.setter
def mag(self, value):
if not isinstance(value, (int, float)):
raise TypeError("Magnitude must be numeric")
if value < 0:
raise ValueError("Magnitude cannot be negative")
self._mag = float(value)
@mag.deleter
def mag(self):
print("Deleting magnitude")
self._mag = None
@property
def depth(self):
return self._depth
@depth.setter
def depth(self, value):
if value < 0:
raise ValueError("Depth cannot be negative")
self._depth = float(value)
eq = Earthquake(7.2, 25.0)
print(eq.mag) # 7.2
eq.mag = 8.1 # setter validates & converts
print(eq.mag) # 8.1
del eq.mag # calls deleter
print(eq.mag) # None
Real-world pattern: earthquake data validation & computed properties — radius ? area, mag ? energy.
class QuakeEvent:
def __init__(self, mag, depth, time):
self._mag = mag
self._depth = depth
self.time = time
@property
def mag(self):
return self._mag
@mag.setter
def mag(self, value):
if not 0 <= value <= 10:
raise ValueError("Magnitude must be between 0 and 10")
self._mag = float(value)
@property
def energy(self):
"""Computed property: energy ~ 10^(1.5 * mag) Joules"""
return 10 ** (1.5 * self._mag)
@property
def is_shallow(self):
return self._depth <= 70.0
# Usage
event = QuakeEvent(7.5, 45.0, "2025-03-01")
print(f"Mag: {event.mag:.1f} ? Energy: {event.energy:.2e} J")
print(f"Shallow? {event.is_shallow}") # True
event.mag = 9.0 # validated
print(f"Updated energy: {event.energy:.2e} J")
Best practices for property() in Python & data workflows. Use @property — for read-only computed or validated attributes. Modern tip: use Polars pl.struct(...).map_elements(...) — for computed columns; Dask .map_partitions() for lazy properties. Prefer @property + @attr.setter — for validation & side effects. Use @attr.deleter — for cleanup logic (rare). Add type hints — @property def mag(self) -> float: .... Use @cached_property — from functools (Python 3.8+) for expensive lazy computation. Use property as function — radius = property(get_radius, set_radius) (legacy style). Avoid heavy computation in getters — cache or use @cached_property. Use properties in dataclasses — @property def area(self) -> float: .... Use properties in Pydantic — @computed_field or @field_validator for similar behavior. Use hasattr(obj, 'prop') — safe property check. Use getattr(obj, 'prop') — dynamic access. Use setattr(obj, 'prop', value) — dynamic set. Use delattr(obj, 'prop') — dynamic delete. Use properties with descriptors — for advanced reuse. Use __slots__ — with properties for memory optimization. Use property in ABCs — enforce interface with abstract properties. Use @abstractproperty — legacy abstract property (use @property + @abstractmethod). Use properties in mixins — share getter/setter logic. Use properties for unit conversion — @property def depth_km(self) -> float: return self._depth / 1000. Use properties for lazy loading — @property def data(self): if not hasattr(self, '_data'): self._data = load(); return self._data.
property() (or @property) turns methods into properties — read-only or read-write attributes with getter/setter/deleter logic, validation, and lazy computation. In 2026, use for encapsulation, computed values, input validation, and integrate with dataclasses/Pydantic/Polars/Dask for clean, type-safe data objects. Master property, and you’ll write elegant, maintainable classes with attribute-like interfaces and powerful behind-the-scenes control.
Next time you want attribute access with logic — use @property. It’s Python’s cleanest way to say: “Treat this method like an attribute — but with full control.”