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(self):
return self._x
x = property(x)
The function named x is passed as the first argument to property, so it becomes the getter.
Properties matter because they let a class expose a clean public attribute, such as user.email or product.price, while retaining control over validation, conversion, lazy calculation, logging, or compatibility with older code.
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
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value
Usage is the same as a normal attribute:
temperature = Temperature(20)
print(temperature.celsius) # 20
temperature.celsius = 25
print(temperature.celsius) # 25
The setter function has the same name as the property because each decorator reassigns the class name celsius to a new property object containing the added accessor.
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.- The second
def size(self, value)creates another function object. It temporarily has the namesizein the class body. @size.setteris evaluated using the current value ofsize, which is the property object from step 2.- The property's
.setter(new_setter_function)method returns a property configured with both getter and setter. - That returned property is assigned to the name
size.
At runtime:
box = Box()
box.size = 8 # calls the setter with value 8
print(box.size) # calls the getter; prints 8
The .deleter form follows the same pattern:
@size.deleter
def size(self):
del self._size
It is approximately size = size.deleter(size_function).
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) # 12
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 User:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
Guard clauses in setters
Setters often reject invalid inputs early, before changing internal state:
@quantity.setter
def quantity(self, value):
if value < 0:
raise ValueError("quantity must be at least zero")
self._quantity = value
Avoid database calls, network requests, or large calculations in ordinary properties unless that behavior is clearly documented. Attribute-looking code is generally expected to be inexpensive and unsurprising.
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
class Item:
@property
def name(self):
return "Notebook"
item = Item()
item.name = "Pen" # AttributeError
Add @name.setter only if assignment should be supported.
Using different property names accidentally
The setter decorator must refer to the existing property name:
@property
def price(self):
return self._price
@cost.setter # NameError: cost does not exist
# def price(self, value): ...
Use @price.setter.
Adding heavy side effects to a property
Avoid properties that silently send requests, modify files, or change unrelated state. Prefer a clearly named method such as refresh_data() when an operation is expensive or has side effects.
Expecting the property to be stored on each instance
The property object is stored on the class. The backing value, such as _x, is usually stored on each instance.
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 @property
| Form | Meaning |
|---|---|
x = property(getx, setx, delx) | Explicitly passes accessor functions to the constructor. |
@property | Equivalent to assigning x = property(x) after defining the getter. |
@x.setter | Calls the existing property's .setter() method with the new setter function. |
@x.deleter | Calls the existing property's .deleter() method with the new deleter function. |
The forms are functionally equivalent. Decorator syntax is generally easier to read because each accessor is written next to its behavior.
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, notobj.value(). - Use a separate backing attribute such as
_valueinside accessors. - A property without a setter is read-only.
- A property without a deleter cannot be deleted.
- A docstring on the getter becomes the property's documentation when no explicit
docargument is supplied. propertyis a descriptor stored on the class, not usually in the instance dictionary.
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 x.
Can a property be read-only?
Yes. Define only the getter with @property. Assigning to it then raises AttributeError unless a setter is added.
Should I always use properties instead of methods?
No. Use a property for attribute-like access that is quick and unsurprising. Use a method when the work is expensive, performs an action, or has significant side effects.
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.