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.
Before this lesson: Python Classes and Objects, Python Encapsulation: Public, Protected and Private
2Python Properties: diagram
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:
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_namenext tofirst_nameandlast_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:
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 dateNothing 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.
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.
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.
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.
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.
- Line 1-3
__init__assignsself.celsius = celsius. Becausecelsiusis a property, this line runs the setter (lines 9-13), so even the very first value is checked. - Line 5-7
@propertyturns this method into the getter. Readingroom.celsius(no parentheses) runs it and returns the value kept in the private attribute_celsius. - Line 9-13
@celsius.setterdefines what happens onroom.celsius = .... It raisesValueErrorfor impossible values; otherwise it stores the value inself._celsius. The different name (_celsius) is what prevents endless recursion. - Line 15-17
fahrenheitis a read-only computed property. No Fahrenheit number is stored anywhere; it is worked out from_celsiusevery time you read it. - Line 19-20Creating the object stores 21 through the setter. Both properties are read without parentheses:
21and21 * 9 / 5 + 32 = 69.8. - Line 21-24Assigning
-300runs the setter, which raisesValueError. Theexceptblock catches it and prints the message. - 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)✓ 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())✓ 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"✓ 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✓ 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 ✓ CompletedThe 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.
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)@property on the line above def area(self): and return self.width * self.height. Do the same for perimeter.class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
return self.width * self.height
@property
def perimeter(self):
return 2 * (self.width + self.height)
room = Rectangle(4, 5)
print(room.area, room.perimeter)
2. Predict the battery level
Predict the output Medium ✓ CompletedThe 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.
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)__init__ also goes through the setter, so 150 is clamped to 100. For the last step work out 15 - 30 first, then let the setter clamp it.100
15 low
0 low
2 more exercises → 5 more examples
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