🎓 A1Learner
🔍

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?