Python Properties
Object-Oriented Python Object-Oriented Python

Python @property: Getters and Setters

1Python Properties: overview

A property is a method that Python lets you use like a plain attribute, so obj.price looks like simple data access while the class quietly computes the value or checks a new one before storing it.

GoalBy the end you can turn a method into a read-only computed attribute with @property, add an @x.setter that validates and raises ValueError, keep the real value in a private _x attribute, avoid the RecursionError trap, and decide when a plain attribute is enough.

Before this lesson: Python Classes and Objects, Python Encapsulation: Public, Protected and Private

2Python Properties: diagram

Python @property: Getters and Settersroom.celsius = 25looks like a plain assignmentPython calls@celsius.setter runsis 25 >= -273.15 ? (if not: raise ValueError)validself._celsius = 25valid: stored in the private attributelater readroom.celsiusthe getter returns self._celsius
Assigning to a property looks like ordinary attribute access, but Python quietly runs your setter first. If the check fails the setter raises ValueError and nothing is stored; if it passes, the value goes into a private attribute that the getter reads back.

3Python Properties: explained step by step

Imagine a Thermostat class that stores a temperature in Celsius. A plain attribute is perfect at first: room.celsius = 21. Then two new requirements arrive. Nobody should be able to store a temperature below absolute zero, and other code also wants the Fahrenheit value. A property lets you add both rules without changing how the class is used.

The problem with plain attributes

A plain attribute accepts anything you hand it:

Python
class Thermostat:
    def __init__(self, celsius):
        self.celsius = celsius

room = Thermostat(21)
room.celsius = -500        # physically impossible, but Python does not mind
print(room.celsius)

Two different situations call for something smarter:

  • The value must stay valid. You want to check every new value before it is stored.
  • The value can be worked out from other data. If you store full_name next to first_name and last_name, the copies drift apart the moment one of them changes. It is safer to compute the value when someone asks for it.

A read-only computed attribute with @property

Put @property above a method that takes only self. Python then lets you read it without parentheses, exactly like a stored attribute:

Python
class Person:
    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}"

asha = Person("Asha", "Rao")
print(asha.full_name)         # no parentheses
asha.last_name = "Verma"
print(asha.full_name)         # always up to date

Nothing called full_name is stored anywhere. The method runs every time you ask, so the answer can never be out of date. Because there is no setter, trying asha.full_name = "Someone Else" fails with AttributeError: property 'full_name' of 'Person' object has no setter. The property is read-only, which is often exactly what you want.

A setter that validates

To make a property writable, add a second method with the same name, decorated with @name.setter. It receives the new value and decides whether to accept it. The real value lives in a separate attribute whose name starts with an underscore (_celsius) — the "internal detail" convention from the encapsulation lesson.

Python
class Thermostat:
    def __init__(self, celsius):
        self.celsius = celsius        # goes through the setter, so it is checked too

    @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

room = Thermostat(21)
room.celsius = 25
print(room.celsius)
try:
    room.celsius = -500
except ValueError as err:
    print("Rejected:", err)
print(room.celsius)

Notice three things. Callers still write room.celsius = 25; nothing about the syntax changed. __init__ assigns to self.celsius (not self._celsius), so even the very first value goes through the check. And the rejected value never reaches _celsius, so the object keeps its last valid state.

Why not get_celsius() and set_celsius()?

In some languages you write getter and setter methods for every field "just in case", because changing a plain field into a method later would break every caller. Python avoids that problem: you start with a plain attribute, and if you later need validation or computation, you turn it into a property. Every line that already uses room.celsius keeps working unchanged. So don't write get_x() and set_x() in Python.

Computing on access (and caching)

A property can do any amount of work: sum a list, format text, combine other attributes. Because it runs on every access, the result is never stale.

Python
class Cart:
    def __init__(self):
        self.prices = []

    @property
    def total(self):
        return round(sum(self.prices, 0.0), 2)

    @property
    def is_empty(self):
        return not self.prices

cart = Cart()
print(cart.is_empty, cart.total)
cart.prices.extend([4.99, 12.5])
print(cart.is_empty, cart.total)

If the work is slow and the data rarely changes, you can remember the answer in a private attribute and clear it whenever the data changes. Python's functools.cached_property does the "compute once" part for you (there is an example on the examples page).

Deleting with @x.deleter

del obj.x normally removes an attribute. For a property you decide what deleting means, usually a bit of clean-up. You will need this rarely, but it completes the set of three hooks: read, write, delete.

Python
class Profile:
    def __init__(self, nickname):
        self._nickname = nickname

    @property
    def nickname(self):
        return self._nickname or "anonymous"

    @nickname.deleter
    def nickname(self):
        print("clearing nickname")
        self._nickname = None

user = Profile("pixel")
print(user.nickname)
del user.nickname
print(user.nickname)

The classic trap: RecursionError

Beginners often write the setter like this (shown as plain text on purpose, because it never finishes):

@celsius.setter
def celsius(self, value):
    self.celsius = value      # WRONG: this assigns to the property again

self.celsius = value is itself an assignment to the property, so Python calls the setter again, which assigns again, which calls the setter again... until Python gives up with RecursionError: maximum recursion depth exceeded. The same happens in the getter if it does return self.celsius. The rule: the property name is the public door; the data lives behind it under a different name, usually _celsius.

When a plain attribute is better

A property costs extra lines, and readers will assume something clever is going on. If a value is just stored and read, with no checks and no calculation, keep it a plain attribute. Start plain; upgrade to a property the day you need one, and no caller will notice.

Also keep properties quick and free of surprises. If reading a value takes seconds, talks to the network or changes other data, make it a method with a verb name (load_report()) so the parentheses warn the reader that work happens.

4Python Properties: syntax

class Name:
    def __init__(self, value):
        self.x = value                # goes through the setter below

    @property
    def x(self):                      # getter: runs on  obj.x
        return self._x

    @x.setter
    def x(self, value):               # setter: runs on  obj.x = value
        if value < 0:
            raise ValueError("x must not be negative")
        self._x = value               # store under a DIFFERENT name (_x)

    @x.deleter
    def x(self):                      # deleter: runs on  del obj.x
        del self._x

    @property
    def double(self):                 # read-only computed attribute
        return self.x * 2

obj = Name(5)
obj.x                                 # 5   (no parentheses)
obj.x = 8                             # runs the setter
obj.double                            # 16

5Python Properties: run the example

Press Run (or Ctrl + Enter). The output (step 6) appears directly below the code. Change anything and run it again.

Interactive example
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius          # runs the setter below

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

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("below absolute zero")
        self._celsius = value

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

room = Temperature(21)
print(room.celsius, room.fahrenheit)
try:
    room.celsius = -300
except ValueError as err:
    print("Rejected:", err)
print(room.celsius)
Expected output21 69.8
Rejected: below absolute zero
21

7Python Properties: how the example works

Hover or tap a step to highlight its lines in the example above.

  1. Line 1-3__init__ assigns self.celsius = celsius. Because celsius is a property, this line runs the setter (lines 9-13), so even the very first value is checked.
  2. Line 5-7@property turns this method into the getter. Reading room.celsius (no parentheses) runs it and returns the value kept in the private attribute _celsius.
  3. Line 9-13@celsius.setter defines what happens on room.celsius = .... It raises ValueError for impossible values; otherwise it stores the value in self._celsius. The different name (_celsius) is what prevents endless recursion.
  4. Line 15-17fahrenheit is a read-only computed property. No Fahrenheit number is stored anywhere; it is worked out from _celsius every time you read it.
  5. Line 19-20Creating the object stores 21 through the setter. Both properties are read without parentheses: 21 and 21 * 9 / 5 + 32 = 69.8.
  6. Line 21-24Assigning -300 runs the setter, which raises ValueError. The except block catches it and prints the message.
  7. Line 25The rejected value never reached _celsius, so the room is still at 21.

8Python Properties: common mistakes

Using the property's own name inside the getter or setter

✗ Wrong

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

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

    @celsius.setter
    def celsius(self, value):
        self.celsius = value

room = Thermostat(21)
RecursionError: maximum recursion depth exceeded

✓ Right

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

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

    @celsius.setter
    def celsius(self, value):
        self._celsius = value

room = Thermostat(21)
print(room.celsius)

self.celsius = value calls the setter again, which assigns again, forever. Keep the real data under a different name, conventionally with a leading underscore, and read and write that name inside the property methods.

Calling a property like a method

✗ Wrong

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14159 * self.radius ** 2

ring = Circle(2)
print(ring.area())
TypeError: 'float' object is not callable

✓ Right

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14159 * self.radius ** 2

ring = Circle(2)
print(ring.area)

A property is read like data. ring.area already runs the method and gives you the number, so the extra () tries to call that number as a function. Drop the parentheses.

Assigning to a property that has no setter

✗ Wrong

class Person:
    def __init__(self, first, last):
        self.first = first
        self.last = last

    @property
    def full_name(self):
        return f"{self.first} {self.last}"

p = Person("Asha", "Rao")
p.full_name = "Asha Verma"
AttributeError: property 'full_name' of 'Person' object has no setter

✓ Right

class Person:
    def __init__(self, first, last):
        self.first = first
        self.last = last

    @property
    def full_name(self):
        return f"{self.first} {self.last}"

    @full_name.setter
    def full_name(self, value):
        self.first, self.last = value.split(" ", 1)

p = Person("Asha", "Rao")
p.full_name = "Asha Verma"
print(p.first, p.last)

A property made with only @property is read-only. Either change the underlying attributes (p.last = "Verma"), or add a @full_name.setter that decides how to split the new value.

Giving the setter a different name from the getter

✗ Wrong

class Item:
    def __init__(self, price):
        self._price = price

    @property
    def price(self):
        return self._price

    @price.setter
    def set_price(self, value):
        self._price = value

item = Item(10)
item.price = 20
AttributeError: property 'price' of 'Item' object has no setter

✓ Right

class Item:
    def __init__(self, price):
        self._price = price

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        self._price = value

item = Item(10)
item.price = 20
print(item.price)

The setter must use exactly the same method name as the property. Here set_price became a separate property, and price is still the original read-only one. Name the setter price.

9Python Properties: exercises

Write your answer, then press Check. The checks run in your browser.

1. Add read-only properties

Write code Easy

The class Rectangle stores width and height. Add two read-only properties: area (width times height) and perimeter (2 times width plus height). They are read without parentheses, for example room.area.

Your code
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    # add the area and perimeter properties here


room = Rectangle(4, 5)
print(room.width, room.height)

2. Predict the battery level

Predict the output Medium

The setter clamps every value into the range 0 to 100. Remember that phone.level -= 30 reads the property, subtracts, then assigns the result back through the setter. Type exactly what the program prints.

Read this code
class Battery:
    def __init__(self, level):
        self.level = level

    @property
    def level(self):
        return self._level

    @level.setter
    def level(self, value):
        self._level = max(0, min(100, value))

    @property
    def status(self):
        return "low" if self._level < 20 else "ok"

phone = Battery(150)
print(phone.level)
phone.level = 15
print(phone.level, phone.status)
phone.level -= 30
print(phone.level, phone.status)

2 more exercises → 5 more examples

Finished this lesson?
Progress is saved in this browser — create a free account to keep it across devices.

Questions learners ask

What does @property do in Python?

@property turns a method into an attribute-like value. You read it as obj.name without parentheses, and Python runs the method behind the scenes. Add @name.setter to run code when someone assigns to it, which lets you validate values or update related data.

Do I need getters and setters in Python?

No. Use plain attributes until you need validation or a computed value, then change that one attribute into a property. Code that already uses obj.name keeps working, so there is no need for get_name() and set_name() methods written in advance.

Why do I get a RecursionError when I use a property setter?

Your getter or setter uses the property's own name, for example self.celsius = value inside the celsius setter, which calls the setter again and again. Store the value in a different attribute such as self._celsius and use that name inside the property methods.

Further reading

10Next lesson

Up nextPython Dunder (Magic) MethodsDunder methods are specially named methods such as __len__ and __add__ that Python calls behind the scenes whenever you…

Related:
Python Class Attributes vs Instance AttributesPython Encapsulation: Public, Protected and PrivatePython Dunder (Magic) Methods

← Python Encapsulation: Public, Protected and Private · All Object-Oriented Python lessons · Practise in the IDE