W3docs

Python @property: Getters and Setters

Learn Python's @property decorator: create getters, setters, deleters, and computed attributes with clean syntax and full validation control.

The @property decorator is Python's built-in mechanism for turning a method into a managed attribute. Instead of writing get_x() and set_x() methods like other languages, you write a normal-looking attribute access (obj.x) while keeping full control over what happens when that attribute is read, written, or deleted.

This chapter covers:

  • Why properties exist and when to use them
  • Creating a read-only property with @property
  • Adding a setter with @<name>.setter
  • Adding a deleter with @<name>.deleter
  • Computed (derived) properties
  • Upgrading a plain attribute to a property without breaking callers
  • The property() built-in function — the decorator's underlying mechanism
  • How properties work as descriptors (brief look under the hood)
  • Common gotchas

Before reading, make sure you are comfortable with Python classes and objects. Properties are a key tool for Python encapsulation. For class-level and static methods, see @staticmethod and @classmethod.

Why Properties Exist

Consider a class that stores a temperature in Celsius. A naive implementation exposes the internal value directly:

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

t = Temperature(25)
t.celsius = -5000   # nothing stops this — physically impossible

The problem: nothing prevents callers from setting a temperature below absolute zero (−273.15 °C). You could add a set_celsius() method with validation, but then callers must change their code from t.celsius = 100 to t.set_celsius(100) — a breaking API change.

@property solves this cleanly. You keep the t.celsius = 100 syntax while adding a layer of control behind the scenes.

Basic Getter: Read-Only Access

The simplest use of @property is a read-only attribute backed by a private variable:

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius   # store in a private attribute

    @property
    def celsius(self):
        return self._celsius

The @property decorator makes celsius look like a plain attribute to the caller:

t = Temperature(25)
print(t.celsius)   # 25  — no parentheses; Python calls the getter automatically

Because there is no setter, attempting to assign to it raises an error:

t.celsius = 30
# AttributeError: property 'celsius' of 'Temperature' object has no setter

This is the correct way to model a value that should only be set at construction time or through specific methods.

Adding a Setter with Validation

Decorate a second method with @<property_name>.setter to handle writes:

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 below absolute zero')
        self._celsius = value

Now both read and write work with plain attribute syntax:

t = Temperature(25)
print(t.celsius)   # 25

t.celsius = 100
print(t.celsius)   # 100

t.celsius = -300   # ValueError: Temperature below absolute zero

Key rule: the setter and getter must share the same name (celsius in both cases). The decorator @celsius.setter links the new method to the existing celsius property object.

Computed Properties

A property does not have to correspond to a stored attribute at all. It can compute a value on the fly from other data:

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 below absolute zero')
        self._celsius = value

    @property
    def fahrenheit(self):
        return self._celsius * 9 / 5 + 32

fahrenheit has no backing variable — it derives its value from _celsius every time it is read:

t = Temperature(0)
print(t.fahrenheit)   # 32.0

t.celsius = 100
print(t.fahrenheit)   # 212.0

Because there is no @fahrenheit.setter, trying to write t.fahrenheit = 100 raises an AttributeError. Computed properties are naturally read-only unless you explicitly add a setter.

A real-world computed property example

class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError('Width must be positive')
        self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value <= 0:
            raise ValueError('Height must be positive')
        self._height = value

    @property
    def area(self):
        return self._width * self._height   # computed; no setter

    @property
    def perimeter(self):
        return 2 * (self._width + self._height)   # computed; no setter


r = Rectangle(4, 5)
print(r.area)       # 20
print(r.perimeter)  # 18

r.width = 10
print(r.area)       # 50

r.width = -1        # ValueError: Width must be positive

Adding a Deleter

The @<property_name>.deleter decorator lets you run code when the caller uses del obj.attr:

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 below absolute zero')
        self._celsius = value

    @celsius.deleter
    def celsius(self):
        print('Deleting celsius')
        del self._celsius


t = Temperature(25)
del t.celsius           # Deleting celsius
print(t.celsius)        # AttributeError: 'Temperature' object has no attribute '_celsius'

Deleters are less commonly used than getters and setters. They are useful when:

  • Removing a cached value to force recomputation on the next access.
  • Explicitly releasing resources tied to an attribute.
  • Enforcing that once deleted, a value cannot be re-read without re-assigning.

Upgrading a Plain Attribute to a Property

One of the biggest practical benefits of @property is that you can start with a plain public attribute and add validation later without changing any calling code. This is sometimes called the uniform access principle.

# Version 1 — plain attribute, no validation
class Circle:
    def __init__(self, radius):
        self.radius = radius

c = Circle(5)
print(c.radius)   # 5
c.radius = 10     # works, but nothing stops c.radius = -1

Later you need validation. With @property you can add it without touching the callers:

# Version 2 — property with validation; public interface unchanged
import math

class Circle:
    def __init__(self, radius):
        self.radius = radius   # this now calls the setter

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError('Radius cannot be negative')
        self._radius = value

    @property
    def area(self):
        return math.pi * self._radius ** 2


c = Circle(5)
print(c.radius)          # 5
print(f'{c.area:.4f}')   # 78.5398

c.radius = 10
print(c.radius)          # 10

c.radius = -1            # ValueError: Radius cannot be negative

Any existing code that reads or writes c.radius continues to work without modification.

The property() Built-in Function

@property is syntactic sugar for the built-in property() function. These two definitions are equivalent:

# --- decorator style (recommended) ---
class Person:
    def __init__(self, age):
        self._age = age

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if not isinstance(value, int) or value < 0:
            raise ValueError('Age must be a non-negative integer')
        self._age = value
# --- property() style (explicit) ---
class Person:
    def __init__(self, age):
        self._age = age

    def _get_age(self):
        return self._age

    def _set_age(self, value):
        if not isinstance(value, int) or value < 0:
            raise ValueError('Age must be a non-negative integer')
        self._age = value

    def _del_age(self):
        del self._age

    age = property(_get_age, _set_age, _del_age, 'The person\'s age in years')

property(fget, fset, fdel, doc) takes up to four arguments: a getter function, a setter function, a deleter function, and a docstring. Any of them can be None.

p = Person(30)
print(p.age)               # 30
p.age = 31
print(p.age)               # 31
print(Person.age.__doc__)  # The person's age in years

The decorator form is cleaner and the standard recommendation. The explicit property() call is useful when you want to pass the docstring without a multi-line decorator block, or when the accessor functions already exist by another name.

How Properties Work: A Brief Look at Descriptors

Internally, property is a descriptor — an object that defines __get__, __set__, and __delete__ on the class. When Python looks up obj.attr, it checks whether the attribute on the class is a descriptor and, if so, calls its __get__ instead of returning the value directly.

You can see this by inspecting the property object on the class:

class Square:
    def __init__(self, side):
        self._side = side

    @property
    def side(self):
        return self._side

    @side.setter
    def side(self, value):
        if value < 0:
            raise ValueError('Side must be non-negative')
        self._side = value


print(type(Square.side))    # <class 'property'>
print(Square.side.fget)     # <function Square.side at 0x...>
print(Square.side.fset)     # <function Square.side at 0x...>
print(Square.side.fdel)     # None

This is why reading Square.side returns the property object itself (descriptor accessed on the class), while reading s.side on an instance triggers __get__ and returns the integer. The descriptor protocol is the same mechanism used by classmethod, staticmethod, and functions themselves. For a deeper dive, see Python magic methods.

Common Gotchas

Infinite recursion: forgetting the underscore

A very common mistake is using the same name for both the property and the backing attribute:

class Bad:
    @property
    def value(self):
        return self.value   # RecursionError! This calls the getter again

    @value.setter
    def value(self, v):
        self.value = v      # RecursionError! This calls the setter again

Always store the backing value in a different name, by convention prefixed with an underscore:

class Good:
    @property
    def value(self):
        return self._value   # reads the private attribute

    @value.setter
    def value(self, v):
        self._value = v      # writes the private attribute

Setter defined before getter

The setter decorator @celsius.setter references the celsius property object, which must exist first. Always define the getter (@property) before the setter and deleter in the class body.

__init__ calls the setter automatically

When you write self.radius = radius inside __init__, Python calls the setter (if one exists). This is usually what you want — validation runs at construction time too. But it means your setter must handle the initial assignment gracefully:

class Circle:
    def __init__(self, radius):
        self.radius = radius   # triggers the setter — validation applies here too

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError('Radius cannot be negative')
        self._radius = value

Circle(-1)   # ValueError: Radius cannot be negative

Properties are class-level, not instance-level

You cannot add a property to a single instance the way you can with regular attributes. Properties are defined on the class and apply to all instances. If you need per-instance attribute customization, see Python dataclasses or use a __slots__-based approach.

Quick Reference

SyntaxWhat it does
@propertyDefines the getter; attribute becomes read-only until a setter is added
@<name>.setterDefines the setter; attribute becomes readable and writable
@<name>.deleterDefines the deleter; del obj.attr triggers this method
property(fget, fset, fdel, doc)Equivalent built-in without decorator syntax
ClassName.prop.fgetThe underlying getter function
ClassName.prop.fsetThe underlying setter function (None if no setter)
ClassName.prop.fdelThe underlying deleter function (None if no deleter)

Practice

Practice
Which decorator do you use to define a setter for a property named `age`?
Which decorator do you use to define a setter for a property named `age`?
Practice
What happens when you assign to a property that has only a getter defined?
What happens when you assign to a property that has only a getter defined?
Practice
You have a plain public attribute `self.radius` in v1 of a class. In v2 you add a `@property` for `radius`. What happens to existing callers that write `obj.radius = 5`?
You have a plain public attribute `self.radius` in v1 of a class. In v2 you add a `@property` for `radius`. What happens to existing callers that write `obj.radius = 5`?
Was this page helpful?