Question
How the @property Decorator Works in Python
Question
How does Python's built-in property work, and why does it appear to receive different arguments when used as a decorator?
For example, property() can be called directly with getter, setter, deleter, and documentation arguments:
class C:
def __init__(self):
self._x = None
def getx(self):
return self._x
def setx(self, value):
self._x = value
def delx(self):
del self._x
x = property(getx, setx, delx, "I'm the 'x' property.")
But the same behavior can be written with decorators:
class C:
def __init__(self):
self._x = None
@property
def x(self):
"""I'm the 'x' property."""
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
When @property decorates x, how is the getter function passed to property? How are the x.setter and x.deleter decorators created?
Short Answer
A property is an object placed on a class that intercepts attribute access. @property is decorator syntax for calling property with the function immediately below it. The resulting property object provides .setter() and .deleter() methods, which return updated property objects and assign them back to the same class attribute name.
Concept
Python attributes do not always have to be plain stored values. A class can define a descriptor—such as a property—that runs code when an attribute is read, assigned, or deleted.
A property can hold up to four pieces of information:
- a getter function (
fget) forobj.x - a setter function (
fset) forobj.x = value - a deleter function (
fdel) fordel obj.x - a documentation string
The direct constructor form is:
x = property(getx, setx, delx, "documentation")
Decorator syntax does not change what property is. It only provides shorter syntax for calling it. This:
@property
def x(self):
return self._x
means approximately:
def x():
._x
x = (x)
Mental Model
Think of a property as a receptionist at a door labelled x.
- Reading
obj.xasks the receptionist to run the getter. - Assigning
obj.x = valueasks the receptionist to run the setter. - Deleting
del obj.xasks the receptionist to run the deleter.
The real value is often stored in a separate internal attribute such as obj._x. The underscore is a convention meaning “internal implementation detail.”
@property hires the receptionist with a getter. @x.setter and @x.deleter add the other instructions to that same property definition.
Syntax and Examples
The constructor signature is conceptually:
property(fget=None, fset=None, fdel=None, doc=None)
Direct constructor form
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
def get_celsius(self):
return self._celsius
def set_celsius(self, value):
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value
celsius = property(get_celsius, set_celsius)
Decorator form
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
():
._celsius
():
value < -:
ValueError()
._celsius = value
Step by Step Execution
Consider this class:
class Box:
@property
def size(self):
return self._size
@size.setter
def size(self, value):
self._size = value
During class creation, Python evaluates it approximately as follows:
class Box:
def size(self):
return self._size
size = property(size)
def size(self, value):
self._size = value
size = size.setter(size)
Step by step:
- The first
def size(self)creates a normal function object. @propertycallsproperty(function)and stores the returned property object under the namesize.
Real World Use Cases
Properties are useful when an attribute needs behavior without making callers use method syntax.
- Validation: reject invalid ages, quantities, dates, or configuration values.
- Normalization: strip whitespace from names or convert text to lowercase before storing it.
- Derived values: expose
rectangle.areacomputed from width and height. - Lazy values: calculate an expensive value only when it is first requested.
- Read-only values: provide a getter but no setter, such as
order.total. - API stability: start with a plain public attribute, then later add validation internally while preserving
obj.namesyntax.
Example of a read-only derived property:
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
return self.width * self.height
rectangle = Rectangle(4, 3)
print(rectangle.area)
Real Codebase Usage
In production code, properties are usually kept small, predictable, and free of surprising work.
Validation at a boundary
A setter protects an object's invariant: a rule that should always remain true.
class Account:
def __init__(self, balance=0):
self.balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if not isinstance(value, (int, float)):
raise TypeError("balance must be a number")
if value < 0:
raise ValueError("balance cannot be negative")
self._balance = value
Read-only public interfaces
A property with no setter communicates that callers should not assign to it.
class :
():
.first_name = first_name
.last_name = last_name
():
Common Mistakes
Recursively accessing the property inside itself
This getter calls itself forever:
class Bad:
@property
def x(self):
return self.x # RecursionError
Use a separate backing attribute instead:
@property
def x(self):
return self._x
Recursively assigning through the property in its setter
@x.setter
def x(self, value):
self.x = value # RecursionError
Store the value in a backing attribute:
@x.setter
def x(self, value):
self._x = value
Forgetting that a getter-only property is read-only
Comparisons
| Approach | Access syntax | Best use | Notes |
|---|---|---|---|
| Plain attribute | obj.x | Simple public data | No automatic validation or computation. |
| Property | obj.x | Controlled attribute access | Can run getter, setter, and deleter code. |
| Regular method | obj.get_x() | Actions or expensive operations | Makes the operation explicit. |
@staticmethod | Class.helper() | Utility not needing instance state | Not related to managed attributes. |
property() versus
Cheat Sheet
class Example:
def __init__(self, value):
self._value = value
@property
def value(self):
return self._value
@value.setter
def value(self, new_value):
self._value = new_value
@value.deleter
def value(self):
del self._value
@propertymeans:value = property(value).@value.settermeans: replacevaluewithvalue.setter(setter_function).@value.deletermeans: replacevaluewithvalue.deleter(deleter_function).- Use
obj.value, not .
FAQ
Is @property a built-in function or a decorator?
property is a built-in class that creates property objects. It can be used as a decorator because decorators are callable objects: @property calls property with the function beneath it.
What does @property pass to property?
It passes the decorated getter function as the first positional argument. @property def x(...) is approximately x = property(x).
Where do .setter and .deleter come from?
They are methods of the property object returned by property(...). Calling them supplies a setter or deleter function and returns a configured property object.
Why is def x written three times?
Each function has a different role: getter, setter, or deleter. The decorators repeatedly bind the name x to the latest property object containing those functions.
Does @x.setter modify the property in place?
Conceptually, treat it as returning a new property object with the added setter. The decorator syntax assigns that returned property back to .
Mini Project
Description
Build a Product class for a small inventory system. Its public price attribute should behave like a normal attribute while preventing invalid prices. A read-only price_with_tax property will demonstrate a computed value.
Goal
Create a validated price property and a read-only computed price_with_tax property.
Requirements
:[
Keep learning
Related questions
@staticmethod vs @classmethod in Python Explained
Learn the difference between @staticmethod and @classmethod in Python with clear examples, use cases, mistakes, and a mini project.
Add Rows to a Pandas DataFrame in Python
Learn how to add rows to a Pandas DataFrame, why repeated row appends are slow, and when to use loc, concat, or record lists.
Call a Function by Name in a Python Module
Learn how to call a function by name in a Python module using strings, getattr, and safe patterns for dynamic function dispatch.