← Back to Python Course | Chapter 7: OOP | Lesson 10 of 11

Python Property Decorators

What is @property?

@property turns a method into something callers access without parentheses, like an ordinary attribute -- def full_name(self): return f'{self.first} {self.last}' becomes obj.full_name instead of obj.full_name() -- which is ideal for read-only computed values.

Example: What is @property?

python
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("Alex", "Doe")
print(p.full_name)

The Property Setter

Adding a matching @name.setter method lets that same attribute accept assignment (obj.balance = 100) while running your own validation code first, such as rejecting a negative balance before it's ever stored -- something plain attribute assignment can't intercept.

Example: The Property Setter

python
class Account:
    def __init__(self, balance):
        self._balance = balance
    @property
    def balance(self):
        return self._balance
    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("Balance cannot be negative")
        self._balance = value

a = Account(100)
a.balance = 200
print(a.balance)

The Property Deleter

A @name.deleter method runs custom cleanup logic when someone writes 'del obj.balance' -- useful for properties backed by external resources (like a cached file handle) that need explicit teardown rather than just vanishing.

Example: The Property Deleter

python
class Account:
    def __init__(self, balance):
        self._balance = balance
    @property
    def balance(self):
        return self._balance
    @balance.deleter
    def balance(self):
        print("Cleaning up balance")
        del self._balance

a = Account(100)
del a.balance

Calculated Properties

Because a property's getter is just a method under the hood, it can compute its return value fresh every time it's accessed -- a .full_name property can always reflect the current first and last name rather than going stale if those change independently.

Example: Calculated Properties

python
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("Alex", "Doe")
p.first = "Sam"
print(p.full_name)

Refactoring Legacy Code

Properties let you convert a plain public attribute into a validated one later without breaking any existing code that reads or writes obj.balance -- callers keep using the same syntax while your class quietly adds validation or computation behind the scenes.

Example: Refactoring Legacy Code

python
class Account:
    def __init__(self, balance):
        self._balance = balance
    @property
    def balance(self):
        return self._balance
    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("Invalid balance")
        self._balance = value

a = Account(100)
a.balance = 50  # still looks like plain attribute access
print(a.balance)

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.