Python Properties
A property is a method that works behind an attribute name. The setter can check a value before storing it.
A plain attribute has no check
This class stores width as a plain attribute.
class Box:
def __init__(self, width):
self.width = width
box = Box(5)
box.width = -5
print(box.width)A property looks like an attribute
The @property line sits above the method. Reading box.width runs the method for you.
class Box:
def __init__(self, width):
self._width = width
@property
def width(self):
return self._width
box = Box(5)
print(box.width)The setter validates
The setter runs on every assignment. It can reject a bad value before storing it.
class CheckedBox:
def __init__(self, width):
self.width = width
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value < 0:
raise ValueError("width cannot be negative")
self._width = value
checked = CheckedBox(4)
checked.width = 8
print(checked.width)
try:
checked.width = -2
except ValueError as error:
print(f"ValueError: {error}")
print(checked.width)A read-only property
This property has no setter. Reading works, but assigning to it fails.
class Square:
def __init__(self, side):
self.side = side
@property
def area(self):
return self.side * self.side
square = Square(3)
print(square.area)
try:
square.area = 20
except AttributeError as error:
print(f"AttributeError: {error}")Callers keep the same code
Both classes below answer to .width. The caller code does not change after the upgrade.
class PlainBox:
def __init__(self, width):
self.width = width
class SmartBox:
def __init__(self, width):
self._width = width
@property
def width(self):
return self._width
print(PlainBox(6).width)
print(SmartBox(6).width)Example
import math
class Circle:
def __init__(self, radius):
self.radius = radius
@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
circle = Circle(2)
print(circle.radius)
print(f"{circle.area:.2f}")
circle.radius = 3
print(circle.radius)
print(f"{circle.area:.2f}")
try:
circle.radius = -1
except ValueError as error:
print(f"ValueError: {error}")
print(circle.radius)
try:
circle.area = 50
except AttributeError as error:
print(f"AttributeError: {error}")Key points
- Property: a method that you use like an attribute
- Use @property to make the getter, the method that reads the value
- Use @radius.setter to make the setter, the method that stores a value
- Read-only property: a property with a getter and no setter
- Use a plain attribute first, then upgrade to a property when checks appear
In this example, Circle stores its radius behind a property. The __init__ method assigns self.radius, so the setter runs at once. The setter rejects a negative value and stores a good one in _radius. The area property has no setter, so it is read-only. It works the area out fresh from the current radius each time. Setting radius to 3 changes the area from 12.57 to 28.27. Setting radius to -1 raises ValueError, and the old value stays. Setting area directly raises AttributeError.
Note
Assigning self.radius inside __init__ runs the setter too.
The first value gets the same check as later values.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Python Playground
Exercise
What happens when you assign a value to a read-only property?