Question

How the @property Decorator Works in Python

pythonpropertiesdecoratorpython-decoratorspython-internals

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) for obj.x
  • a setter function (fset) for obj.x = value
  • a deleter function (fdel) for del 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.x asks the receptionist to run the getter.
  • Assigning obj.x = value asks the receptionist to run the setter.
  • Deleting del obj.x asks 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.

Take Quiz

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:

  1. The first def size(self) creates a normal function object.
  2. @property calls property(function) and stores the returned property object under the name size.
  3. The second def size(self, value) creates another function object. It temporarily has the name size in the class body.
  4. @size.setter is evaluated using the current value of size, which is the property object from step 2.
  5. The property's .setter(new_setter_function) method returns a property configured with both getter and setter.
  6. 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.area computed 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.name syntax.

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
Take Quiz

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

ApproachAccess syntaxBest useNotes
Plain attributeobj.xSimple public dataNo automatic validation or computation.
Propertyobj.xControlled attribute accessCan run getter, setter, and deleter code.
Regular methodobj.get_x()Actions or expensive operationsMakes the operation explicit.
@staticmethodClass.helper()Utility not needing instance stateNot related to managed attributes.

property() versus @property

FormMeaning
x = property(getx, setx, delx)Explicitly passes accessor functions to the constructor.
@propertyEquivalent to assigning x = property(x) after defining the getter.
@x.setterCalls the existing property's .setter() method with the new setter function.
@x.deleterCalls 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.

Take Quiz

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
  • @property means: value = property(value).
  • @value.setter means: replace value with value.setter(setter_function).
  • @value.deleter means: replace value with value.deleter(deleter_function).
  • Use obj.value, not obj.value().
  • Use a separate backing attribute such as _value inside 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 doc argument is supplied.
  • property is 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.

Take Quiz

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

:[

Take Quiz

Keep learning

Related questions

View more